Phase 1 stabilization. Removes the four-copy prompt/agent-description drift surface and the silent router fallback. - models.py: hoist model IDs to module-level constants; add with_retries() helper (2 retries on Anthropic+OpenAI transient errors via with_retry). - agents.py: single AGENTS dict (model_fn, prompt, description) and a make_agent_node() factory that collapses six near-identical node functions. - graph.py: router prompt is generated from AGENTS; router_node uses with_structured_output(RouteDecision) and returns an explicit "unknown" route instead of the silent "researcher" fallback. New unknown_node wires to END. All LLM invocations go through with_retries. - state.py: add "unknown" to the route Literal. - run.py: --route-only now imports router_node from graph.py, killing the fourth prompt copy. - tests/: pytest golden-set (20 labelled tasks + size guard). Skips cleanly without ANTHROPIC_API_KEY or COMPOSIO_API_KEY. Validated 21/21 passing.
90 lines
2.3 KiB
Python
90 lines
2.3 KiB
Python
import os
|
|
|
|
from langchain_anthropic import ChatAnthropic
|
|
from langchain_core.runnables import Runnable
|
|
from langchain_google_genai import ChatGoogleGenerativeAI
|
|
from langchain_openai import ChatOpenAI
|
|
|
|
# Model IDs — single source of truth. Bump here when families ship new revs.
|
|
CLAUDE_SONNET = "claude-sonnet-4-20250514"
|
|
CLAUDE_HAIKU = "claude-haiku-4-5-20251001"
|
|
OPENAI_CROSS_REVIEWER = "gpt-4.1"
|
|
GEMINI_SCANNER = "gemini-2.5-pro"
|
|
DEEPSEEK_FAST_CODER = "deepseek-coder"
|
|
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
|
|
|
|
|
|
def _collect_retriable_exceptions() -> tuple[type[BaseException], ...]:
|
|
excs: list[type[BaseException]] = []
|
|
try:
|
|
import anthropic
|
|
|
|
excs.extend(
|
|
[
|
|
anthropic.APIConnectionError,
|
|
anthropic.RateLimitError,
|
|
anthropic.InternalServerError,
|
|
]
|
|
)
|
|
except ImportError:
|
|
pass
|
|
try:
|
|
import openai
|
|
|
|
excs.extend(
|
|
[
|
|
openai.APIConnectionError,
|
|
openai.RateLimitError,
|
|
openai.InternalServerError,
|
|
]
|
|
)
|
|
except ImportError:
|
|
pass
|
|
return tuple(excs)
|
|
|
|
|
|
RETRIABLE_EXCEPTIONS = _collect_retriable_exceptions()
|
|
|
|
|
|
def with_retries(runnable: Runnable) -> Runnable:
|
|
"""Wrap an LLM runnable with up to 2 retries on transient provider errors."""
|
|
if not RETRIABLE_EXCEPTIONS:
|
|
return runnable
|
|
return runnable.with_retry(
|
|
retry_if_exception_type=RETRIABLE_EXCEPTIONS,
|
|
stop_after_attempt=3,
|
|
wait_exponential_jitter=True,
|
|
)
|
|
|
|
|
|
def get_orchestrator():
|
|
return ChatAnthropic(model=CLAUDE_SONNET, temperature=0)
|
|
|
|
|
|
def get_implementer():
|
|
return ChatAnthropic(model=CLAUDE_SONNET, temperature=0)
|
|
|
|
|
|
def get_reviewer():
|
|
return ChatAnthropic(model=CLAUDE_SONNET, temperature=0)
|
|
|
|
|
|
def get_researcher():
|
|
return ChatAnthropic(model=CLAUDE_HAIKU, temperature=0)
|
|
|
|
|
|
def get_cross_reviewer():
|
|
return ChatOpenAI(model=OPENAI_CROSS_REVIEWER, temperature=0.2)
|
|
|
|
|
|
def get_scanner():
|
|
return ChatGoogleGenerativeAI(model=GEMINI_SCANNER, temperature=0)
|
|
|
|
|
|
def get_fast_coder():
|
|
return ChatOpenAI(
|
|
model=DEEPSEEK_FAST_CODER,
|
|
base_url=DEEPSEEK_BASE_URL,
|
|
api_key=os.getenv("DEEPSEEK_API_KEY"),
|
|
temperature=0,
|
|
)
|