How Team 1's Code Actually Fits Together
Not the architecture diagram again — this is a walk through the real function calls, in the order they actually execute, with short excerpts from the real files in sinder38/Team-1-Prac-A-Project. Anyone who can read GitHub should be able to open the same files and follow along.
The Call Chain, End to End
One weekly run is a single call to run_pipeline.py. Here's what actually happens, in order:
main()reads a TOML config file (which stages are on, which LLM models to run)- It creates one empty
PipelineContextobject for this week - It calls
run_almanac,run_technical,run_macro,run_evidencein sequence — each one fills in one field of that same context object - It calls
run_llmonce per configured model — each call reads the same shared context and builds a fresh prompt from whatever's in it so far - It writes a comparison table markdown file summarizing all models side by side
PipelineContext object.
The Shared Contract — base.py + schemas.py
Every agent (technical, macro, almanac, evidence, and every LLM) inherits from one abstract base class:
backend/agents/base.pyclass BaseAgent(ABC, Generic[T]):
agent_type: str
@abstractmethod
def run(self, prediction_date: date, **kwargs) -> T: ...
@abstractmethod
def render_md(self, output: T, prediction_date: date) -> str: ...
That's the entire contract: every agent must implement run() (do the work, return typed output) and render_md() (turn that output into a human-readable report). Nothing else is required, which is why five very different agents — a yfinance technical analyzer, a seasonal almanac lookup, and four different LLM wrappers — can all be called the exact same way by the pipeline.
Every agent's output is a typed Python dataclass, not a loose dictionary:
backend/agents/schemas.py@dataclass
class TechnicalOutput:
prediction_date: date
instruments: dict[str, InstrumentTechnical]
agent_type: str = "technical"
TechnicalOutput, that's a Python error caught immediately —
not a silent KeyError or a missing value discovered three steps later.
The Pipeline Context — the entire object is 6 lines
This is the object every agent reads from and writes to. It's deliberately small:
backend/agents/pipeline/context.py@dataclass
class PipelineContext:
prediction_date: date
almanac: AlmanacOutput | None = None
technical: TechnicalOutput | None = None
macro: MacroOutput | None = None
evidence: EvidenceOutput | None = None
llm_outputs: list[LLMOutput] = field(default_factory=list)
Every field starts as None. As each stage function runs, it fills in exactly one field. By the time the LLM stage runs, whatever fields are filled in get assembled into its prompt — and whatever's still None just gets skipped, no special-casing required.
run_pipeline.py passes
agent outputs directly as function arguments. It works at 3 tickers and 3 agents, but it's
exactly the kind of thing that gets messy fast if a 4th or 5th agent gets added later.
Stage Functions — one per agent, all the same shape
Every data agent's stage function follows an identical 4-line pattern:
backend/agents/pipeline/stages.pydef run_technical(ctx: PipelineContext, config: dict) -> None:
agent = TechnicalAgent()
output = agent.run(ctx.prediction_date)
ctx.technical = output # ← writes into the shared context
_save_artifacts(agent, output, ctx.prediction_date, config)
almanac, macro, and evidence are the exact same four lines with different class names. The LLM stage is slightly different because it needs to know which model to run:
backend/agents/pipeline/stages.pyLLM_REGISTRY: dict[str, Callable[[], BaseLLMAgent]] = {
"nemotron": lambda: _make_openrouter("NVIDIA Nemotron 3 Super", "nvidia/nemotron-3-super-120b-a12b:free"),
"gptoss": lambda: _make_openrouter("OpenAI gpt-oss-120b", "openai/gpt-oss-120b:free"),
"gemma": lambda: _make_openrouter("Google Gemma 4 31B", "google/gemma-4-31b-it:free"),
"laguna": lambda: _make_openrouter("Poolside Laguna M.1", "poolside/laguna-m.1:free"),
}
Each value is a zero-argument function that builds an agent instance when called — not the agent itself. That means listing all four models here costs nothing until run_llm actually calls one of these lambdas, which is also why adding a fifth model later is a one-line addition, not a refactor.
The Entry Point — config-driven, and it stops on the first real error
run_pipeline.py reads which stages are enabled from a TOML file, then runs them in a fixed order:
backend/run_pipeline.pyfor name, fn in stage_map.items():
if not stages.get(name, False):
print(f"[pipeline] skipping {name} (disabled in config)")
continue
try:
fn(ctx, config)
except Exception as e:
print(f"[pipeline] ERROR in {name}: {e}", file=sys.stderr)
sys.exit(1) # ← one broken agent stops the whole run
The LLM Layer — Deliberately Split in Two Files
base_llm.py and multi_model_runner.py have a strict division of labor, and it's written explicitly as a code comment, not just implied:
backend/agents/llm/multi_model_runner.py"""
CONTRACT: we own ONLY query() on the BaseLLMAgent subclass. We do NOT modify any
other base_llm method (build_prompt / parse_response / run / render_md).
"""
base_llm.py owns: assembling the prompt from the shared context, and parsing whatever text comes back into a typed LLMOutput. This code is identical no matter which of the four models is being called.
multi_model_runner.py owns: only the actual network call — OpenRouterAgent.query() — sending the prompt to OpenRouter and returning raw text. It does no parsing at all; that's explicitly not its job.
The Prompt Format Decision — and why it matters for MPE specifically
This is the single most useful thing in the whole codebase for MPE right now. Team 1's system prompt to every model is explicit:
backend/agents/llm/multi_model_runner.pysystem_instruction = (
"You are a strict financial data formatter. "
"You MUST output exactly the requested keys in PLAIN TEXT format, separated by colons. "
"DO NOT OUTPUT JSON FORMAT. "
"For index ranges, you MUST strictly use the word 'to' (e.g., -1.5 to 2.0). "
"Do NOT wrap your response in markdown code blocks."
)
And the parser on the receiving end is deliberately forgiving — it scans every line for a KEY: value pattern anywhere in the text, tolerating whatever reasoning or extra prose surrounds it:
backend/agents/llm/base_llm.pylines = {
line.split(":", 1)[0].strip(): line.split(":", 1)[1].strip()
for line in raw.strip().splitlines()
if ":" in line
}
Failure Handling Philosophy — honest failure over silent failure
Three separate defensive layers in OpenRouterAgent.query():
backend/agents/llm/multi_model_runner.pyfor attempt in range(max_retries):
try:
response = self.client.chat.completions.create(...)
if not response.choices or not response.choices[0].message.content:
raise RuntimeError(f"[{self.model_name}] Empty response from provider.")
return response.choices[0].message.content
except Exception as e:
if attempt == max_retries - 1:
raise RuntimeError(f"...Exhausted all {max_retries} API retries.") from e
time.sleep(2 ** attempt) # 1s, 2s, 4s
Every exception gets retried with exponential backoff — not just rate-limit errors specifically. And when a model fails for good, it isn't dropped quietly. It gets a visible marker in the final comparison table:
backend/agents/llm/multi_model_runner.pydef _failed_row(exc: Exception) -> dict:
"""Every data cell reads FAILED so a reviewer can't mistake it for 'no data'."""
marker = f"❌ FAIL ({type(exc).__name__})"
row = {key: marker for _, key in COMPARISON_DIMENSIONS}
return row
And the whole run exits with a non-zero code if any model failed — which is what makes their CI turn red on a bad run, rather than a red flag getting buried in a log nobody reads.
__main__ block admits an unresolved bug in their
own system: a path-resolution issue in base_llm.py that the pre-flight check
doesn't actually catch. Worth knowing this isn't a polished, bug-free reference —
it's a real, evolving student codebase with its own open issues, same as MPE.
The Validation Layer
scripts/rules/ defines format rules per agent type, and validate_output.py runs them with three severity levels — PASS, WARN, FAIL — against any saved report file:
scripts/validate_output.pyExit codes:
0 all required checks pass (warnings may be present)
1 one or more required checks failed
2 usage error
This is a separate, standalone script from the pipeline itself — it's meant to be run in CI against whatever markdown report an agent just wrote, checking things like "does this file actually contain the required fields in the required format," independent of whether the pipeline run itself succeeded.
What MPE Should Actually Borrow
| Pattern | Why it's worth taking |
|---|---|
Plain-text KEY: value prompt format instead of JSON |
Directly fixes the exact Nemotron/Laguna parsing failures MPE hit today — this is the highest-value, lowest-effort change available |
| Exponential backoff on any exception, not just 429 | MPE currently only retries on rate-limit errors specifically; other transient failures get zero retries |
| Honest failure markers instead of silent skip | MPE currently drops a failed model with no visible trace in the output; a reviewer can't tell "1 model failed" from "we only ever use 1 model" |
Shared PipelineContext object |
Lower priority at 3 tickers/3 agents, but worth adopting before adding a 4th agent |
build_prompt) and one system instruction string, and it targets
the exact failure mode already confirmed in MPE's own committed logs.