Skip to content

Commit d0bf48f

Browse files
feat(day6): TranslatorAgent, QuestionGenerator pipeline, Groq fallback for CN desk
- agents/translator_agent.py: thesis → PredictionMarketQuestion via Groq LLM - Polymarket-shaped binary questions, time-bounded 30-90 days - Oracle-resolvable criteria with cited data sources - markets/question_generator.py: full pipeline orchestrator (analyze → translate → pin) - Routes to US/CN/CRYPTO agent by region key - IPFS pins both thesis and question artifacts - agents/china_agent.py: Groq fallback when DEEPSEEK_API_KEY absent - tests/test_day6.py: 9 new tests (agent routing, parser, fallback, instantiation) - All 30 tests pass Co-Authored-By: AdaL <adal@sylph.ai>
1 parent d4ce016 commit d0bf48f

3 files changed

Lines changed: 439 additions & 0 deletions

File tree

agents/translator_agent.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Translator Agent — converts an InvestmentThesis into a PredictionMarketQuestion.
2+
3+
This agent is region-agnostic: it accepts a thesis from any desk (US, CN, CRYPTO, EU)
4+
and produces a Polymarket-shaped binary question that is:
5+
- Time-bounded (expiry ≤ 90 days out)
6+
- Oracle-resolvable (objective YES/NO criteria citing a data source)
7+
- Language-normalised to English
8+
9+
Model routing (per AGENTS.md):
10+
- Default: Groq / llama-3.3-70b-versatile (fast, free-tier)
11+
- Override via model_client / model_kwargs constructor args.
12+
13+
Run standalone:
14+
uv run python -m agents.translator_agent --ticker AAPL
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import argparse
20+
import asyncio
21+
import json
22+
import logging
23+
from datetime import datetime, timedelta, timezone
24+
from typing import Any
25+
26+
from dotenv import load_dotenv
27+
28+
load_dotenv()
29+
30+
import adalflow as adal
31+
32+
from agents.base_agent import PydanticJsonParser
33+
from reasoning.trace_schema import Direction, InvestmentThesis, PredictionMarketQuestion
34+
35+
logger = logging.getLogger(__name__)
36+
37+
# ---------------------------------------------------------------------------
38+
# Prompt template
39+
# ---------------------------------------------------------------------------
40+
41+
_TRANSLATOR_TEMPLATE = """\
42+
<SYS>
43+
You are a prediction-market question designer. Your job is to convert an investment thesis
44+
into a single, binary, oracle-resolvable prediction-market question.
45+
46+
Rules:
47+
1. The question MUST be answerable YES or NO by a neutral third party using public data.
48+
2. The question MUST be time-bounded — expiry within 30–90 days from today ({{today}}).
49+
3. The resolution_criteria MUST cite a specific, publicly accessible data source
50+
(e.g. "Bloomberg consensus EPS estimate", "CoinGecko daily close", "PBOC official announcement").
51+
4. category must be one of: macro | earnings | policy | crypto | geopolitics
52+
5. translation_confidence: how confident you are this question faithfully captures the thesis (0.0–1.0).
53+
54+
Output ONLY strict JSON matching this schema (no extra text):
55+
{{schema}}
56+
</SYS>
57+
58+
Investment thesis to translate:
59+
Ticker: {{ticker}}
60+
Region: {{region}}
61+
Direction: {{direction}}
62+
Confidence score: {{confidence_score}}
63+
Summary (EN): {{summary_en}}
64+
Risk factors: {{risks}}
65+
Source thesis ID: {{thesis_id}}
66+
Source language: {{lang}}
67+
Translated by model: {{model_name}}\
68+
"""
69+
70+
71+
class TranslatorAgent(adal.Component):
72+
"""Converts an InvestmentThesis → PredictionMarketQuestion via LLM."""
73+
74+
def __init__(
75+
self,
76+
*,
77+
model_client: adal.ModelClient | None = None,
78+
model_kwargs: dict[str, Any] | None = None,
79+
) -> None:
80+
super().__init__()
81+
client = model_client or adal.GroqAPIClient() # type: ignore[attr-defined]
82+
kwargs = model_kwargs or {"model": "llama-3.3-70b-versatile", "temperature": 0.1, "max_tokens": 1024}
83+
84+
schema_str = json.dumps(
85+
PredictionMarketQuestion.model_json_schema(), indent=2
86+
)
87+
template = _TRANSLATOR_TEMPLATE.replace("{{schema}}", schema_str)
88+
89+
self._generator = adal.Generator(
90+
model_client=client,
91+
model_kwargs=kwargs,
92+
template=template,
93+
)
94+
self._parser = PydanticJsonParser(PredictionMarketQuestion)
95+
self._model_name = kwargs.get("model", "llama-3.3-70b-versatile")
96+
97+
def translate(self, thesis: InvestmentThesis) -> PredictionMarketQuestion | None:
98+
"""Synchronously translate a thesis into a prediction-market question."""
99+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
100+
101+
risks = "; ".join(thesis.risk_factors) if thesis.risk_factors else "none provided"
102+
103+
output = self._generator(
104+
prompt_kwargs={
105+
"today": today,
106+
"ticker": thesis.ticker_or_asset,
107+
"region": thesis.region.value,
108+
"direction": thesis.direction.value,
109+
"confidence_score": f"{thesis.confidence_score:.2f}",
110+
"summary_en": thesis.thesis_summary_en[:800],
111+
"risks": risks[:400],
112+
"thesis_id": thesis.thesis_id,
113+
"lang": thesis.working_language,
114+
"model_name": self._model_name,
115+
}
116+
)
117+
118+
question = self._parser.call(
119+
output,
120+
extra_fields={
121+
"source_thesis_id": thesis.thesis_id,
122+
"source_language": thesis.lang,
123+
"translated_by_model": self._model_name,
124+
},
125+
)
126+
127+
if not isinstance(question, PredictionMarketQuestion):
128+
logger.warning("TranslatorAgent failed to parse PredictionMarketQuestion. raw: %s",
129+
getattr(output, "raw_response", "")[:300])
130+
return None
131+
132+
return question
133+
134+
async def atranslate(self, thesis: InvestmentThesis) -> PredictionMarketQuestion | None:
135+
"""Async wrapper — runs translate in executor to avoid blocking event loop."""
136+
loop = asyncio.get_event_loop()
137+
return await loop.run_in_executor(None, self.translate, thesis)
138+
139+
140+
# ---------------------------------------------------------------------------
141+
# CLI smoke test
142+
# ---------------------------------------------------------------------------
143+
144+
145+
async def _main() -> None:
146+
from reasoning.trace_schema import AssetClass, Direction, Region
147+
148+
parser = argparse.ArgumentParser()
149+
parser.add_argument("--ticker", default="AAPL")
150+
parser.add_argument("--verbose", action="store_true")
151+
args = parser.parse_args()
152+
153+
logging.basicConfig(
154+
level=logging.DEBUG if args.verbose else logging.INFO,
155+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
156+
)
157+
158+
# Construct a dummy thesis for smoke-testing
159+
from datetime import datetime, timezone
160+
thesis = InvestmentThesis(
161+
ticker=args.ticker,
162+
region=Region.US,
163+
asset_class=AssetClass.EQUITY,
164+
direction=Direction.LONG,
165+
conviction=0.75,
166+
summary=f"{args.ticker} shows strong earnings momentum with AI tailwinds.",
167+
catalysts=["Q2 earnings beat expected", "AI capex cycle acceleration"],
168+
risk_factors=["Fed rate hike", "regulatory headwinds"],
169+
lang="en",
170+
)
171+
172+
agent = TranslatorAgent()
173+
question = await agent.atranslate(thesis)
174+
if question:
175+
print(question.model_dump_json(indent=2))
176+
else:
177+
print("Translation failed — check logs.")
178+
179+
180+
def run() -> None:
181+
asyncio.run(_main())
182+
183+
184+
if __name__ == "__main__":
185+
run()

markets/question_generator.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Question Generator — orchestrates the full thesis→market pipeline.
2+
3+
Pipeline:
4+
1. Run a regional agent (US / CN / CRYPTO) to produce an InvestmentThesis.
5+
2. Pass the thesis to TranslatorAgent to produce a PredictionMarketQuestion.
6+
3. Pin both artifacts to IPFS and record a TraceMetadata entry.
7+
8+
This module is the single entry-point for the end-to-end demo.
9+
10+
Run standalone:
11+
GROQ_API_KEY=xxx uv run python -m markets.question_generator --ticker AAPL --region us
12+
GROQ_API_KEY=xxx uv run python -m markets.question_generator --ticker ETH --region crypto
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
import asyncio
19+
import json
20+
import logging
21+
from typing import Literal
22+
23+
from dotenv import load_dotenv
24+
25+
load_dotenv()
26+
27+
from agents.translator_agent import TranslatorAgent
28+
from reasoning import ipfs_pinner
29+
from reasoning.trace_schema import InvestmentThesis, PredictionMarketQuestion
30+
31+
logger = logging.getLogger(__name__)
32+
33+
RegionKey = Literal["us", "cn", "crypto"]
34+
35+
36+
def _build_agent(region: RegionKey): # type: ignore[return]
37+
"""Instantiate the correct regional agent for *region*."""
38+
if region == "us":
39+
from agents.us_agent import USAgent
40+
return USAgent()
41+
if region == "cn":
42+
from agents.china_agent import ChinaAgent
43+
return ChinaAgent()
44+
if region == "crypto":
45+
from agents.crypto_agent import CryptoAgent
46+
return CryptoAgent()
47+
raise ValueError(f"Unknown region: {region!r}. Choose from: us, cn, crypto")
48+
49+
50+
async def generate(
51+
ticker: str,
52+
region: RegionKey = "us",
53+
*,
54+
pin: bool = True,
55+
) -> tuple[InvestmentThesis, PredictionMarketQuestion | None]:
56+
"""Full pipeline: analyze → translate → (optional) pin to IPFS.
57+
58+
Returns:
59+
(thesis, question) — question is None if translation fails.
60+
"""
61+
# 1. Regional analysis
62+
agent = _build_agent(region)
63+
logger.info("Running %s agent on %s…", region.upper(), ticker)
64+
thesis = await agent.analyze(ticker)
65+
logger.info("Thesis: %s %s (conviction=%.2f)", ticker, thesis.direction.value, thesis.conviction)
66+
67+
# 2. Translate to prediction-market question
68+
translator = TranslatorAgent()
69+
logger.info("Translating thesis → prediction-market question…")
70+
question = await translator.atranslate(thesis)
71+
if question:
72+
logger.info("Question: %s", question.question_text)
73+
else:
74+
logger.warning("Translation produced no question.")
75+
76+
# 3. IPFS pinning (soft-fail)
77+
if pin:
78+
thesis_cid = await ipfs_pinner.pin_json(thesis.model_dump(mode="json"), name=f"thesis-{thesis.thesis_id[:8]}")
79+
logger.info("Thesis pinned → %s", thesis_cid)
80+
81+
if question:
82+
q_cid = await ipfs_pinner.pin_json(question.model_dump(mode="json"), name=f"question-{question.question_id[:8]}")
83+
logger.info("Question pinned → %s", q_cid)
84+
85+
return thesis, question
86+
87+
88+
# ---------------------------------------------------------------------------
89+
# CLI
90+
# ---------------------------------------------------------------------------
91+
92+
93+
async def _main() -> None:
94+
parser = argparse.ArgumentParser(description="Full thesis → prediction-market pipeline.")
95+
parser.add_argument("--ticker", default="AAPL")
96+
parser.add_argument("--region", choices=["us", "cn", "crypto"], default="us")
97+
parser.add_argument("--no-pin", action="store_true", help="Skip IPFS pinning")
98+
parser.add_argument("--verbose", action="store_true")
99+
args = parser.parse_args()
100+
101+
logging.basicConfig(
102+
level=logging.DEBUG if args.verbose else logging.INFO,
103+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
104+
)
105+
106+
thesis, question = await generate(args.ticker, args.region, pin=not args.no_pin)
107+
108+
print("\n" + "=" * 60)
109+
print("INVESTMENT THESIS")
110+
print("=" * 60)
111+
print(thesis.model_dump_json(indent=2, ensure_ascii=False))
112+
113+
if question:
114+
print("\n" + "=" * 60)
115+
print("PREDICTION MARKET QUESTION")
116+
print("=" * 60)
117+
print(question.model_dump_json(indent=2))
118+
119+
120+
def run() -> None:
121+
asyncio.run(_main())
122+
123+
124+
if __name__ == "__main__":
125+
run()

0 commit comments

Comments
 (0)