Skip to content

Commit 65e89ae

Browse files
committed
pre-refactor dump
1 parent c24bb9f commit 65e89ae

File tree

12 files changed

+1681
-664
lines changed

12 files changed

+1681
-664
lines changed

core/agent/project.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,20 @@ async def run_agent_stream(user_prompt: str, chat_history: Optional[str] = None,
648648
if ev == "on_chat_model_stream":
649649
data = event.get("data") if isinstance(event, dict) else getattr(event, "data", {})
650650
chunk = (data or {}).get("chunk") if isinstance(data, dict) else getattr(data, "chunk", None)
651-
text = getattr(chunk, "content", None)
652-
if isinstance(text, str) and text:
651+
content = getattr(chunk, "content", None)
652+
653+
# Handle both string content (OpenAI) and list content (Anthropic)
654+
text = None
655+
if isinstance(content, str):
656+
text = content
657+
elif isinstance(content, list) and content:
658+
# Anthropic format: [{'text': '...', 'type': 'text', 'index': 0}]
659+
for item in content:
660+
if isinstance(item, dict) and item.get("type") == "text":
661+
text = item.get("text", "")
662+
break
663+
664+
if text:
653665
yielded_any = True
654666
yield text
655667
except Exception:

core/agent/simple.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class AgentResult(BaseModel):
5050
# ---- Runtime cache ----
5151
PRELOADED_TOOLS: List[Any] = []
5252
RUN_TRACES: List[ToolTrace] = []
53+
DOMAIN_PROMPTS_TEXT: str = ""
5354

5455

5556
def _get_env(key: str, default: Optional[str] = None) -> Optional[str]:
@@ -136,19 +137,32 @@ def _runner(**kwargs):
136137

137138

138139
def initialize_runtime(tool_root: Optional[str] = None) -> None:
139-
global PRELOADED_TOOLS
140+
global PRELOADED_TOOLS, DOMAIN_PROMPTS_TEXT
140141
try:
141142
exts = discover_extensions(tool_root)
142143
except Exception:
143144
exts = []
144145
tools: List[Any] = []
146+
domain_prompts: List[str] = []
145147
for ext in exts:
146148
for fn in (ext.get("tools") or []):
147149
try:
148150
tools.append(_wrap_callable_as_tool(fn, ext.get("name", "unknown")))
149151
except Exception:
150152
continue
153+
# Collect full domain system prompts
154+
try:
155+
name = ext.get("name", "")
156+
sp = ext.get("system_prompt", "")
157+
if isinstance(name, str) and isinstance(sp, str) and sp.strip():
158+
domain_prompts.append(f"[Domain: {name}]\n{sp.strip()}")
159+
except Exception:
160+
pass
151161
PRELOADED_TOOLS = tools
162+
try:
163+
DOMAIN_PROMPTS_TEXT = "\n\n".join([p for p in domain_prompts if p])
164+
except Exception:
165+
DOMAIN_PROMPTS_TEXT = ""
152166

153167

154168
def _active_models() -> Dict[str, str]:
@@ -192,7 +206,12 @@ async def run_agent(user_prompt: str, chat_history: Optional[str] = None, memory
192206
if isinstance(llm, str) and llm.strip() in {"low", "med", "high"}
193207
else get_chat_model(role="domain", model=_get_env("REACT_MODEL", "gpt-4.1"), callbacks=[LLMRunTracer("react")], temperature=0.0)
194208
)
195-
agent = create_react_agent(model, tools=tools)
209+
# Include combined domain system prompts as the agent's system prompt when available
210+
if isinstance(DOMAIN_PROMPTS_TEXT, str) and DOMAIN_PROMPTS_TEXT.strip():
211+
final_prompt = "Domain system prompts:\n" + DOMAIN_PROMPTS_TEXT.strip()
212+
agent = create_react_agent(model, tools=tools, prompt=final_prompt)
213+
else:
214+
agent = create_react_agent(model, tools=tools)
196215
except Exception as e:
197216
msg = f"Error building ReAct agent: {str(e)}"
198217
return AgentResult(final=msg, results=[], timings=[], content=msg, response_time_secs=0.0, traces=[])
@@ -215,11 +234,11 @@ async def run_agent(user_prompt: str, chat_history: Optional[str] = None, memory
215234
import asyncio
216235
t0 = time.perf_counter()
217236
try:
218-
result = await agent.ainvoke({"messages": messages}, config={"recursion_limit": 8, "callbacks": [LLMRunTracer("react")]})
237+
result = await agent.ainvoke({"messages": messages}, config={"recursion_limit": 16, "callbacks": [LLMRunTracer("react")]})
219238
except RuntimeError:
220239
# fallback loop handling if needed
221240
loop = asyncio.get_event_loop()
222-
result = await agent.ainvoke({"messages": messages}, config={"recursion_limit": 8, "callbacks": [LLMRunTracer("react")]})
241+
result = await agent.ainvoke({"messages": messages}, config={"recursion_limit": 16, "callbacks": [LLMRunTracer("react")]})
223242
elapsed = time.perf_counter() - t0
224243

225244
# Extract final content
@@ -249,6 +268,93 @@ async def run_agent(user_prompt: str, chat_history: Optional[str] = None, memory
249268
)
250269

251270

271+
async def run_agent_stream(user_prompt: str, chat_history: Optional[str] = None, memory: Optional[str] = None, tool_root: Optional[str] = None, llm: Optional[str] = None):
272+
"""Yield incremental text chunks while the agent generates a response.
273+
274+
Fallback: if streaming is unavailable, yields the final response once.
275+
"""
276+
# Discover/warm tools if not already done or if tool_root differs
277+
if not PRELOADED_TOOLS or isinstance(tool_root, str):
278+
initialize_runtime(tool_root=tool_root)
279+
tools = PRELOADED_TOOLS or []
280+
if not tools:
281+
yield "No tools discovered. Ensure files matching *_tool.py exist under extensions/."
282+
return
283+
284+
# Build a simple ReAct agent with all tools
285+
try:
286+
from langgraph.prebuilt import create_react_agent
287+
# Use tier if provided; else fall back to env model
288+
model = (
289+
get_chat_model(role="domain", tier=llm, callbacks=[LLMRunTracer("react")], temperature=0.0)
290+
if isinstance(llm, str) and llm.strip() in {"low", "med", "high"}
291+
else get_chat_model(role="domain", model=_get_env("REACT_MODEL", "gpt-4.1"), callbacks=[LLMRunTracer("react")], temperature=0.0)
292+
)
293+
# Include combined domain system prompts as the agent's system prompt when available
294+
if isinstance(DOMAIN_PROMPTS_TEXT, str) and DOMAIN_PROMPTS_TEXT.strip():
295+
final_prompt = "Domain system prompts:\n" + DOMAIN_PROMPTS_TEXT.strip()
296+
agent = create_react_agent(model, tools=tools, prompt=final_prompt)
297+
else:
298+
agent = create_react_agent(model, tools=tools)
299+
except Exception:
300+
# If building agent fails, just yield non-streaming result from run_agent
301+
res = await run_agent(user_prompt, chat_history=chat_history, memory=memory, tool_root=tool_root, llm=llm)
302+
yield res.final
303+
return
304+
305+
# Prepare messages
306+
from langchain_core.messages import SystemMessage, HumanMessage
307+
messages: List[Any] = []
308+
if chat_history or memory:
309+
messages.append(SystemMessage(content=(
310+
"Conversation context to consider when responding.\n"
311+
f"Chat history:\n{chat_history or ''}\n\n"
312+
f"Memory:\n{memory or ''}"
313+
)))
314+
messages.append(HumanMessage(content=user_prompt))
315+
316+
# Clear traces for this run
317+
del RUN_TRACES[:]
318+
319+
yielded_any = False
320+
try:
321+
# Prefer event-streaming for token deltas
322+
async for event in agent.astream_events({"messages": messages}, config={"recursion_limit": 16, "callbacks": [LLMRunTracer("react")]}, version="v1"):
323+
try:
324+
ev = event.get("event") if isinstance(event, dict) else getattr(event, "event", None)
325+
if ev == "on_chat_model_stream":
326+
data = event.get("data") if isinstance(event, dict) else getattr(event, "data", {})
327+
chunk = (data or {}).get("chunk") if isinstance(data, dict) else getattr(data, "chunk", None)
328+
content = getattr(chunk, "content", None)
329+
330+
# Handle both string content (OpenAI) and list content (Anthropic)
331+
text = None
332+
if isinstance(content, str):
333+
text = content
334+
elif isinstance(content, list) and content:
335+
# Anthropic format: [{'text': '...', 'type': 'text', 'index': 0}]
336+
for item in content:
337+
if isinstance(item, dict) and item.get("type") == "text":
338+
text = item.get("text", "")
339+
break
340+
341+
if text:
342+
yielded_any = True
343+
yield text
344+
except Exception:
345+
# Ignore malformed events; continue streaming
346+
continue
347+
except Exception:
348+
# If streaming path fails, fall back to single-shot
349+
pass
350+
351+
if not yielded_any:
352+
# Fallback to non-streaming execution
353+
res = await run_agent(user_prompt, chat_history=chat_history, memory=memory, tool_root=tool_root, llm=llm)
354+
yield res.final
355+
return
356+
357+
252358
def main(argv: Optional[List[str]] = None) -> int:
253359
import argparse
254360
parser = argparse.ArgumentParser(description="Simple ReAct agent over all tools")

core/agent/simple_passthough.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,123 @@ async def run_agent(user_prompt: str, chat_history: Optional[str] = None, memory
458458
)
459459

460460

461+
async def run_agent_stream(user_prompt: str, chat_history: Optional[str] = None, memory: Optional[str] = None, tool_root: Optional[str] = None, llm: Optional[str] = None):
462+
"""Yield incremental text chunks while the agent generates a response.
463+
464+
For this planner-executor architecture, we stream tool results as they complete.
465+
Fallback: if streaming is unavailable, yields the final response once.
466+
"""
467+
# Prepare tools once
468+
if not TOOL_RUNNERS or isinstance(tool_root, str):
469+
initialize_runtime(tool_root=tool_root)
470+
if not TOOL_RUNNERS:
471+
yield "No tools discovered. Ensure files matching *_tool.py exist under extensions/."
472+
return
473+
474+
# Planner model: use tier if provided; otherwise env-based model
475+
tracer = LLMRunTracer("planner")
476+
if isinstance(llm, str) and llm.strip() in {"low", "med", "high"}:
477+
model = get_chat_model(role="domain", tier=llm.strip(), callbacks=[tracer], temperature=0.0)
478+
else:
479+
planner_model_name = _get_env("MONO_PT_PLANNER_MODEL", _get_env("REACT_MODEL", "gpt-4.1")) or "gpt-4.1"
480+
model = get_chat_model(role="domain", model=planner_model_name, callbacks=[tracer], temperature=0.0)
481+
482+
# Clear traces per run
483+
del RUN_TRACES[:]
484+
485+
# Iterative plan-execute-review loop with streaming
486+
accumulated_segments: List[str] = []
487+
recursion_limit = int(_get_env("MONO_PT_RECURSION_LIMIT", "8") or 8)
488+
followup_items: List[ToolResult] = []
489+
step = 0
490+
yielded_any = False
491+
492+
while step < recursion_limit:
493+
step += 1
494+
# Build planning messages (initial or follow-up)
495+
messages = _build_planner_messages(
496+
user_prompt=user_prompt,
497+
chat_history=chat_history,
498+
memory=memory,
499+
light_schema=LIGHT_SCHEMA,
500+
review_items=(followup_items or None),
501+
)
502+
503+
# Invoke planner
504+
_dbg_print(f"[simple-pt-stream] step {step}: planning...")
505+
plan_resp = await model.ainvoke(messages)
506+
507+
# Parse plan JSON
508+
raw_text = (plan_resp.content or "") if hasattr(plan_resp, "content") else str(plan_resp)
509+
_dbg_print(f"[simple-pt-stream] step {step}: planner raw -> {_truncate(raw_text, int(_get_env('MONO_PT_LOG_MAXLEN', '600') or '600'))}")
510+
parsed = _extract_json_object(raw_text)
511+
planner_step = PlannerStep()
512+
if isinstance(parsed, dict):
513+
try:
514+
planner_step = PlannerStep.model_validate(parsed)
515+
except Exception:
516+
# Try to coerce structure
517+
calls_raw = parsed.get("calls") if isinstance(parsed.get("calls"), list) else []
518+
calls: List[PlannedToolCall] = []
519+
for c in calls_raw:
520+
try:
521+
calls.append(PlannedToolCall.model_validate(c))
522+
except Exception:
523+
continue
524+
planner_step = PlannerStep(calls=calls, final_text=parsed.get("final_text"))
525+
526+
# If planner provided final text only and no calls, finish
527+
if not planner_step.calls:
528+
if isinstance(planner_step.final_text, str) and planner_step.final_text.strip():
529+
final_chunk = planner_step.final_text.strip()
530+
accumulated_segments.append(final_chunk)
531+
yield final_chunk
532+
yielded_any = True
533+
_dbg_print(f"[simple-pt-stream] step {step}: final_text provided; finishing.")
534+
break
535+
536+
# Execute calls concurrently
537+
_dbg_print(f"[simple-pt-stream] step {step}: executing {len(planner_step.calls)} call(s) concurrently...")
538+
for idx, pc in enumerate(planner_step.calls, start=1):
539+
try:
540+
args_str = json.dumps(pc.args or {}, ensure_ascii=False)
541+
except Exception:
542+
args_str = str(pc.args or {})
543+
_dbg_print(f"[simple-pt-stream] step {step} CALL {idx}/{len(planner_step.calls)}: tool={pc.tool} passthrough={(pc.options.passthrough if pc.options else True)} args={_truncate(args_str, int(_get_env('MONO_PT_LOG_MAXLEN', '600') or '600'))}")
544+
545+
_, paired = await _execute_planned_calls(planner_step.calls)
546+
547+
# Route outputs and stream passthrough results
548+
followup_items = []
549+
for idx, (pc, res) in enumerate(paired, start=1):
550+
passthrough = True if pc.options is None else bool(getattr(pc.options, "passthrough", True))
551+
if passthrough and res.success:
552+
# Stream directly by yielding
553+
if isinstance(res.public_text, str) and res.public_text.strip():
554+
chunk_text = res.public_text.strip()
555+
accumulated_segments.append(chunk_text)
556+
yield chunk_text
557+
yielded_any = True
558+
_dbg_print(f"[simple-pt-stream] step {step} RESULT {idx}/{len(paired)}: tool={res.tool} success={res.success} passthrough={passthrough} ROUTE=STREAMED text={_truncate(res.public_text, int(_get_env('MONO_PT_LOG_MAXLEN', '600') or '600'))}")
559+
else:
560+
followup_items.append(res)
561+
_dbg_print(f"[simple-pt-stream] step {step} RESULT {idx}/{len(paired)}: tool={res.tool} success={res.success} passthrough={passthrough} ROUTE=REVIEW text={_truncate(res.public_text, int(_get_env('MONO_PT_LOG_MAXLEN', '600') or '600'))}")
562+
563+
# If nothing needs review, continue next plan step
564+
if not followup_items:
565+
_dbg_print(f"[simple-pt-stream] step {step}: no follow-up needed; finishing.")
566+
break
567+
568+
if not yielded_any:
569+
# Fallback to non-streaming execution
570+
_dbg_print("[simple-pt-stream] no content streamed; falling back to non-streaming.")
571+
res = await run_agent(user_prompt, chat_history=chat_history, memory=memory, tool_root=tool_root, llm=llm)
572+
yield res.final
573+
return
574+
575+
_dbg_print(f"[simple-pt-stream] done. steps={step} segments={len(accumulated_segments)}")
576+
577+
461578
def main(argv: Optional[List[str]] = None) -> int:
462579
import argparse
463580
parser = argparse.ArgumentParser(description="Simple passthough agent over all tools")

core/helpers/llm_selector.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,27 @@
66
"gpt-4.1-nano",
77
"gpt-4.1-mini",
88
"gpt-4.1",
9+
"gpt-5"
910
}
1011

1112
SUPPORTED_GEMINI = {
1213
"gemini-2.5-flash",
1314
"gemini-2.5-flash-lite",
14-
"gemini-2.5-pro-thinking",
15+
"gemini-2.5-pro",
1516
}
1617

17-
SUPPORTED_ANTHROPIC = {
18-
"claude-3.5-sonnet",
19-
"claude-4.0-sonnet",
18+
SUPPORTED_ANTHROPIC = { # Alias for claude-sonnet-4-20250514
19+
"claude-sonnet-4-20250514", # Alias for claude-sonnet-4-5-20250929
20+
"claude-sonnet-4-5-20250929", # Claude Sonnet 4.5 full ID
21+
2022
}
2123

2224

2325
# Simple fixed tier mapping (no overrides for now)
2426
TIER_TO_MODEL = {
2527
"low": "gpt-4.1",
26-
"med": "claude-4.0-sonnet",
27-
"high": "gemini-2.5-pro-thinking",
28+
"med": "claude-sonnet-4-0", # Claude Sonnet 4.0
29+
"high": "claude-sonnet-4-5", # Claude Sonnet 4.5
2830
}
2931

3032

@@ -62,13 +64,13 @@ def get_chat_model(*, role: str, model: Optional[str] = None, tier: Optional[str
6264
# Heuristic: choose provider based on model identifier prefix/allowlist
6365
if selected in SUPPORTED_OPENAI or selected.lower().startswith("gpt"):
6466
from langchain_openai import ChatOpenAI
65-
return ChatOpenAI(model=selected, temperature=temperature, callbacks=cbs)
67+
return ChatOpenAI(model=selected, temperature=temperature, callbacks=cbs, streaming=True)
6668

6769
if selected in SUPPORTED_ANTHROPIC or selected.lower().startswith("claude"):
6870
from langchain_anthropic import ChatAnthropic
6971

7072
api_key = _env("ANTHROPIC_API_KEY")
71-
kwargs = {"model": selected, "temperature": temperature, "callbacks": cbs}
73+
kwargs = {"model": selected, "temperature": temperature, "callbacks": cbs, "streaming": True}
7274
if api_key:
7375
kwargs["anthropic_api_key"] = api_key
7476
return ChatAnthropic(**kwargs)
@@ -85,7 +87,7 @@ def get_chat_model(*, role: str, model: Optional[str] = None, tier: Optional[str
8587
# Allow explicit API key passthrough via env, but ChatGoogleGenerativeAI also reads GOOGLE_API_KEY
8688
api_key = _env("GOOGLE_API_KEY") or _env("GEMINI_API_KEY")
8789
if api_key:
88-
return ChatGoogleGenerativeAI(model=selected, temperature=temperature, callbacks=cbs, api_key=api_key)
89-
return ChatGoogleGenerativeAI(model=selected, temperature=temperature, callbacks=cbs)
90+
return ChatGoogleGenerativeAI(model=selected, temperature=temperature, callbacks=cbs, api_key=api_key, streaming=True)
91+
return ChatGoogleGenerativeAI(model=selected, temperature=temperature, callbacks=cbs, streaming=True)
9092

9193

0 commit comments

Comments
 (0)