Investment Committee¶
| Profile | AutoAgent + CustomAgent (mixed) |
| Infra required | Redis (auto-detected), yfinance |
| Agents | 7 specialist agents |
| Run | cd examples/investment_committee && python committee.py --mode full --ticker NVDA |
What it does¶
The flagship example. Seven specialist agents form a real investment committee that deliberates over a stock allocation decision. Each agent plays a distinct professional role — market analyst, financial analyst, technical analyst, risk officer, knowledge agent, memo writer, and committee chair.
This is the only example that mixes AutoAgent and CustomAgent profiles in the same mesh, and uses the most complex workflow DAG across all examples.
market_analysis ──┐
financial_analysis ─┼──→ risk_assessment ──┐
technical_analysis ─┤ │
knowledge_retrieval┘ memo_draft ←────────┘
↓
final_decision (committee chair)
The committee chair reads the full memo and all prior analyses, then outputs a structured BUY / HOLD / PASS decision with allocation amount, conviction level, and conditions.
Run modes¶
cd examples/investment_committee
# Quick mode — fundamentals only (1 step, fast)
python committee.py --mode quick --ticker AAPL
# Full mode — complete deliberation pipeline (6 steps)
python committee.py --mode full --ticker NVDA --amount 1500000
python committee.py --mode full --ticker AMD --amount 2000000
Key pattern: mixed profiles in one mesh¶
from jarviscore import Mesh
mesh = Mesh(config={"redis_url": REDIS_URL}) # (1)!
for AgentClass in [
MarketAnalystAgent, # AutoAgent — uses yfinance + LLM analysis
FinancialAnalystAgent, # AutoAgent — pulls P/E, P/S, EV/EBITDA
TechnicalAnalystAgent, # AutoAgent — RSI, MA crossover, trend
RiskOfficerAgent, # AutoAgent — VaR, mandate compliance
KnowledgeAgent, # CustomAgent — reads from LTM, no LLM needed (2)!
MemoWriterAgent, # AutoAgent — synthesises all prior outputs
CommitteeChairAgent, # AutoAgent — final BUY/HOLD/PASS decision
]:
mesh.add(AgentClass)
await mesh.start()
- No
mode=argument.Mesh()detects Redis and activates the workflow engine with persistence automatically. KnowledgeAgentis aCustomAgentthat reads from long-term memory — mixing profiles lets you use the right tool for each role.
Key pattern: complex fan-in DAG¶
steps = [
{"id": "market_analysis", "agent": "market_analyst", "task": "..."},
{"id": "financial_analysis", "agent": "financial_analyst", "task": "..."},
{"id": "technical_analysis", "agent": "technical_analyst", "task": "..."},
{"id": "knowledge_retrieval","agent": "knowledge_agent", "task": "..."},
{
"id": "risk_assessment",
"agent": "risk_officer",
"task": "Assess risk for a {amount} USD position in {ticker} ...",
"depends_on": ["market_analysis", "financial_analysis"], # (1)! fan-in
"params": params,
},
{
"id": "memo_draft",
"agent": "memo_writer",
"task": "Write a formal investment memo ...",
"depends_on": [ # (2)! convergence
"market_analysis", "financial_analysis",
"technical_analysis", "knowledge_retrieval", "risk_assessment",
],
"params": params,
},
{
"id": "final_decision",
"agent": "committee_chair",
"task": "Review the memo and make the final allocation decision ...",
"depends_on": ["memo_draft"],
"params": params,
},
]
results = await mesh.workflow(wf_id, steps)
risk_assessmentfans in from two parallel analyses. TheWorkflowEnginewaits for both before dispatching.memo_draftis the convergence point — it waits for all five preceding steps and receives all their outputs asprevious_step_resultsin the execution context.
Key pattern: processing AutoAgent output¶
AutoAgent steps return their result via sandbox execution. The LLM sometimes wraps the JSON in a Markdown code fence (```json ... ```), so accessing result["output"] directly may give you a string instead of a dict. Use _extract_output() to handle both:
import json, re
def _extract_output(result: dict) -> dict:
"""Return the step output as a dict, parsing JSON string if needed."""
raw = result.get("output", {})
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
clean = re.sub(r"^```[a-z]*\s*|\s*```$", "", raw.strip(), flags=re.MULTILINE).strip()
try:
parsed = json.loads(clean)
if isinstance(parsed, dict):
return parsed
except (json.JSONDecodeError, ValueError):
pass
return {}
# Index results by step id, then extract safely
by_id = {r.get("step_id") or steps[i]["id"]: r for i, r in enumerate(results)}
dec = by_id.get("final_decision", {})
out = _extract_output(dec) # (1)!
print(f"Action: {out.get('action')}")
print(f"Allocation: ${out.get('allocation_usd', 0):,.0f}")
print(f"Conviction: {out.get('conviction')}")
_extract_output()strips Markdown fences, attemptsjson.loads(), and falls back to an empty dict — so the rest of your code never has to branch on whether the output is a string or a dict.
The same pattern applies inside CommitteeChairAgent when reading the memo from a prior step:
Expected output (full mode)¶
==============================
Investment Committee | FULL | NVDA | $1,500,000
==============================
┌─ COMMITTEE DECISION ──────────────────────────
│ Ticker: NVDA
│ Action: BUY
│ Allocation: $1,200,000
│ Conviction: HIGH
│ Rationale: Strong AI infrastructure tailwinds, dominant market position,
│ acceptable valuation given growth trajectory.
│ Conditions: Monitor Q3 earnings; set stop-loss at $850
└──────────────────────────────────────────────────────
Full memo written to data/memos/
File structure¶
examples/investment_committee/
├── committee.py ← entry point (run this)
├── portfolio.json ← mandate + current holdings
├── agents/
│ ├── base.py ← shared CommitteeAutoAgent base class
│ ├── committee_chair.py
│ ├── financial_analyst.py
│ ├── knowledge_agent.py ← CustomAgent (reads LTM)
│ ├── market_analyst.py
│ ├── memo_writer.py
│ ├── risk_officer.py
│ └── technical_analyst.py
└── dashboard.py ← optional rich terminal dashboard