[2026.03 Week 4] Five Trending Repos of the Week
Five GitHub repositories trending this week.
TradingAgents (⭐ 43.5k). Multi-agent framework where LLM analysts debate before making investment decisions.
cc-switch (⭐ 34.9k). Desktop proxy for Claude Code, Codex, and OpenCode with circuit breaker failover.
MiroFish (⭐ 45k). Swarm intelligence engine that simulates thousands of AI agents to predict real-world outcomes.
Project N.O.M.A.D. (⭐ 19.3k). Offline-first knowledge server with local AI and a searchable knowledge base.
OpenDataLoader PDF (⭐ 10.3k). PDF parser that routes complex pages to AI backends while keeping simple ones on a fast Java path.
TauricResearch/TradingAgents
⭐ 43.5k · Python
TradingAgents turns stock analysis into a staged debate across specialist LLM agents. Four analysts write reports, bull and bear researchers argue over them, three risk analysts review the trade, and a portfolio manager makes the final call.
Each analyst starts with a role prompt and tool access. The sentiment analyst looks like this.
TauricResearch/TradingAgents:tradingagents/agents/analysts/social_media_analyst.py:L17-L20
system_message = (
"You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Provide specific, actionable insights with supporting evidence to help traders make informed decisions."
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
)The analyst is not given pre-fetched data. It gets a get_news(query, start_date, end_date) tool and decides what to ask for. The other analysts follow the same pattern, so the reports are generated on demand.
Those four reports are then dropped into the bull researcher’s prompt, along with debate history and a memory lookup.
TauricResearch/TradingAgents:tradingagents/agents/researchers/bull_researcher.py:L13-L43
market_research_report = state["market_report"]
sentiment_report = state["sentiment_report"]
news_report = state["news_report"]
fundamentals_report = state["fundamentals_report"]
curr_situation = f"{market_research_report}\n\n{sentiment_report}\n\n{news_report}\n\n{fundamentals_report}"
past_memories = memory.get_memories(curr_situation, n_matches=2)
# ...
prompt = f"""You are a Bull Analyst advocating for investing in the stock. ...
Resources available:
Market research report: {market_research_report}
Social media sentiment report: {sentiment_report}
Latest world affairs news: {news_report}
Company fundamentals report: {fundamentals_report}
Conversation history of the debate: {history}
Last bear argument: {current_response}
Reflections from similar situations and lessons learned: {past_memory_str}
...
"""The reports go in as raw text, with no summary step.
Prompting does most of the orchestration. Bull and bear researchers see the same reports and history but get opposite instructions. The three risk analysts also share context, then argue from aggressive, conservative, and neutral positions.
The portfolio manager then has to commit to a structured output and a five-level rating.
TauricResearch/TradingAgents:tradingagents/agents/managers/portfolio_manager.py:L24-L53
prompt = f"""As the Portfolio Manager, synthesize the risk analysts' debate and deliver the final trading decision.
# ...
**Rating Scale** (use exactly one):
- **Buy**: Strong conviction to enter or add to position
- **Overweight**: Favorable outlook, gradually increase exposure
- **Hold**: Maintain current position, no action needed
- **Underweight**: Reduce exposure, take partial profits
- **Sell**: Exit position or avoid entry
**Context:**
- Trader's proposed plan: **{trader_plan}**
- Lessons from past decisions: **{past_memory_str}**
**Required Output Structure:**
1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell.
2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon.
3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate and past reflections.
# ...
Be decisive and ground every conclusion in specific evidence from the analysts."""Overweight and Underweight give the model room between buy and sell. past_memory_str carries forward lessons from similar past trades, so the final recommendation is shaped by prior outcomes.
farion1231/cc-switch
⭐ 34.9k · Rust
cc-switch is a Tauri desktop app that proxies requests from Claude Code, Codex, or OpenCode to multiple providers. When one provider starts failing, it trips a circuit breaker and moves traffic to the next option in the failover queue.
The core logic is the standard Closed/Open/HalfOpen state machine. Every request passes through allow_request.
farion1231/cc-switch:src-tauri/src/proxy/circuit_breaker.rs:L144-L187
pub async fn allow_request(&self) -> AllowResult {
let state = *self.state.read().await;
match state {
CircuitState::Closed => AllowResult { allowed: true, used_half_open_permit: false },
CircuitState::Open => {
let config = self.config.read().await;
if let Some(opened_at) = *self.last_opened_at.read().await {
if opened_at.elapsed().as_secs() >= config.timeout_seconds {
drop(config);
self.transition_to_half_open().await;
return match *self.state.read().await {
CircuitState::HalfOpen => self.allow_half_open_probe(),
// ...
};
}
}
AllowResult { allowed: false, used_half_open_permit: false }
}
CircuitState::HalfOpen => self.allow_half_open_probe(),
}
}Closed lets everything through. Open rejects requests until the timeout expires, then flips to HalfOpen and allows one probe request. allow_half_open_probe() uses AtomicU32::fetch_add so only one request tests the recovering provider. drop(config) releases the RwLock read guard before the state write, which avoids a deadlock. The state is then re-read because another thread may already have finished the transition.
666ghj/MiroFish
⭐ 45k · Python
MiroFish turns source documents such as news, policy drafts, and financial reports into a knowledge graph, generates agent personas from that graph, and simulates how those agents react over time.
A big part of the realism is the round scheduler.
666ghj/MiroFish:backend/scripts/run_parallel_simulation.py:L1040-L1080
def get_active_agents_for_round(env, config, current_hour, round_num):
time_config = config.get("time_config", {})
base_min = time_config.get("agents_per_hour_min", 5)
base_max = time_config.get("agents_per_hour_max", 20)
peak_hours = time_config.get("peak_hours", [9, 10, 11, 14, 15, 20, 21, 22])
off_peak_hours = time_config.get("off_peak_hours", [0, 1, 2, 3, 4, 5])
if current_hour in peak_hours:
multiplier = time_config.get("peak_activity_multiplier", 1.5)
elif current_hour in off_peak_hours:
multiplier = time_config.get("off_peak_activity_multiplier", 0.3)
else:
multiplier = 1.0
target_count = int(random.uniform(base_min, base_max) * multiplier)
# ...
for cfg in agent_configs:
if current_hour not in cfg.get("active_hours", list(range(8, 23))):
continue
if random.random() < cfg.get("activity_level", 0.5):
candidates.append(cfg.get("agent_id", 0))
return random.sample(candidates, min(target_count, len(candidates)))The current hour sets a global multiplier, each agent checks its own active_hours, and the survivors roll against activity_level. That gives each round a different population and produces a believable daily activity curve.
Those agent personas come from the graph builder. Seed documents go into Zep, the pipeline creates a graph, sets an ontology, chunks text, ingests batches, waits for processing, then reads back entities and edges.
666ghj/MiroFish:backend/app/services/graph_builder.py:L115-L173
# 1. Create graph
graph_id = self.create_graph(graph_name)
# 2. Set ontology
self.set_ontology(graph_id, ontology)
# 3. Chunk text
chunks = TextProcessor.split_text(text, chunk_size, chunk_overlap)
# 4. Batch-insert into Zep
episode_uuids = self.add_text_batches(
graph_id, chunks, batch_size,
lambda msg, prog: self.task_manager.update_task(
task_id, progress=20 + int(prog * 0.4), message=msg
)
)
# 5. Wait for processing
self._wait_for_episodes(
episode_uuids,
lambda msg, prog: self.task_manager.update_task(
task_id, progress=60 + int(prog * 0.3), message=msg
)
)
# 6. Extract graph info
graph_info = self._get_graph_info(graph_id)The ontology is LLM-generated. The model is asked for 10 entity types and relationship types, and each extracted entity later becomes one simulated agent.
After the simulation, the ReportAgent queries the graph through a ReAct loop with four tools.
666ghj/MiroFish:backend/app/services/report_agent.py:L918-L953
def _define_tools(self) -> Dict[str, Dict[str, Any]]:
return {
"insight_forge": {
"name": "insight_forge",
"description": TOOL_DESC_INSIGHT_FORGE,
"parameters": {
"query": "The question or topic to analyze in depth",
"report_context": "Current report section context (optional)"
}
},
"panorama_search": {
"name": "panorama_search",
"description": TOOL_DESC_PANORAMA_SEARCH,
"parameters": {
"query": "Search query for relevance ranking",
"include_expired": "Include expired/historical content (default True)"
}
},
"quick_search": { ... },
"interview_agents": {
"name": "interview_agents",
"description": TOOL_DESC_INTERVIEW_AGENTS,
"parameters": {
"interview_topic": "Interview topic (e.g. 'student reactions to the dorm incident')",
"max_agents": "Max agents to interview (default 5, max 10)"
}
}
}insight_forge breaks a question into sub-queries, runs semantic search, extracts entities, and builds relationship chains such as Professor Zhang --[CRITICIZED]--> University Admin. panorama_search widens the search to include historical facts. interview_agents queries the simulation itself for quotes from specific agents. Each section gets up to five tool calls (MAX_TOOL_CALLS_PER_SECTION = 5) before the report is written.
Crosstalk-Solutions/project-nomad
⭐ 19.3k · TypeScript
Project N.O.M.A.D. is an offline server built on AdonisJS, React, Ollama, and Qdrant. It bundles AI chat, local knowledge search, downloadable maps, and survival reference tools with no telemetry.
Its chat endpoint does four things in sequence: open the SSE stream, rewrite the query, run retrieval, and scale the injected context to the selected model.
Crosstalk-Solutions/project-nomad:admin/app/controllers/ollama_controller.ts:L31-L41
async chat({ request, response }: HttpContext) {
const reqData = await request.validateUsing(chatSchema)
// Flush SSE headers immediately so the client connection is open while
// pre-processing (query rewriting, RAG lookup) runs in the background.
if (reqData.stream) {
response.response.setHeader('Content-Type', 'text/event-stream')
response.response.setHeader('Cache-Control', 'no-cache')
response.response.setHeader('Connection', 'keep-alive')
response.response.flushHeaders()
}Flushing headers first opens the EventSource connection immediately, so the UI stays responsive while query rewrite and retrieval finish.
In multi-turn chat, a small model rewrites the latest message into a standalone search query.
Crosstalk-Solutions/project-nomad:admin/app/controllers/ollama_controller.ts:L215-L265
async chat({ request, response }: HttpContext) {
const reqData = await request.validateUsing(chatSchema)
// Flush SSE headers immediately so the client connection is open while
// pre-processing (query rewriting, RAG lookup) runs in the background.
if (reqData.stream) {
response.response.setHeader('Content-Type', 'text/event-stream')
response.response.setHeader('Cache-Control', 'no-cache')
response.response.setHeader('Connection', 'keep-alive')
response.response.flushHeaders()
}Flushing headers first opens the EventSource connection immediately, so the UI stays responsive while query rewrite and retrieval finish.
In multi-turn chat, a small model rewrites the latest message into a standalone search query.
Crosstalk-Solutions/project-nomad:admin/app/controllers/ollama_controller.ts:L215-L265
private async rewriteQueryWithContext(messages: Message[]): Promise<string | null> {
// Get recent conversation history (last 6 messages for 3 turns)
const recentMessages = messages.slice(-6)
// Skip rewriting for short conversations. Rewriting adds latency with
// little RAG benefit until there is enough context to matter.
const userMessages = recentMessages.filter(msg => msg.role === 'user')
if (userMessages.length <= 2) {
return userMessages[userMessages.length - 1]?.content || null
}
const conversationContext = recentMessages
.map(msg => {
const role = msg.role === 'user' ? 'User' : 'Assistant'
// Truncate assistant messages to first 200 chars to keep context manageable
const content = msg.role === 'assistant'
? msg.content.slice(0, 200) + (msg.content.length > 200 ? '...' : '')
: msg.content
return `${role}: "${content}"`
})
.join('\n')
// ...
const response = await this.ollamaService.chat({
model: DEFAULT_QUERY_REWRITE_MODEL, // qwen2.5:3b
messages: [
{ role: 'system', content: SYSTEM_PROMPTS.query_rewrite },
{ role: 'user', content: `Conversation:\n${conversationContext}\n\nRewritten Query:` },
],
})Short conversations skip rewriting because the latest message is usually self-contained. Longer ones send only the last 6 messages to a small 3B model (qwen2.5:3b), with assistant replies truncated to 200 characters.
It also sizes RAG context from the model name itself.
Crosstalk-Solutions/project-nomad:admin/app/controllers/ollama_controller.ts:L200-L213 and admin/constants/ollama.ts:L71-L75
private getContextLimitsForModel(modelName: string): { maxResults: number; maxTokens: number } {
const sizeMatch = modelName.match(/(\d+\.?\d*)[bB]/)
const paramBillions = sizeMatch ? parseFloat(sizeMatch[1]) : 8
for (const tier of RAG_CONTEXT_LIMITS) {
if (paramBillions <= tier.maxParams) {
return { maxResults: tier.maxResults, maxTokens: tier.maxTokens }
}
}
return { maxResults: 5, maxTokens: 0 }
}
// constants/ollama.ts
export const RAG_CONTEXT_LIMITS = [
{ maxParams: 3, maxResults: 2, maxTokens: 1000 }, // 1-3B models
{ maxParams: 8, maxResults: 4, maxTokens: 2500 }, // 4-8B models
{ maxParams: Infinity, maxResults: 5, maxTokens: 0 }, // 13B+ (no cap)
]A regex pulls the number before b from names like llama3.2:3b or qwen2.5:1.5b. Small models get fewer results and tighter token caps. Large models get more context, and the top result is always kept even when a cap applies. The same code scales from a Raspberry Pi to a multi-GPU box without manual tuning.
opendataloader-project/opendataloader-pdf
⭐ 10.3k · Java
OpenDataLoader PDF extracts text, tables, and structure from PDFs into Markdown, JSON, or HTML. In hybrid mode, simple pages stay on the local Java path and complex ones go to an AI backend.
That router is a per-page triage cascade. Signals are checked in priority order and return early with a confidence score.
opendataloader-project/opendataloader-pdf:.../hybrid/TriageProcessor.java:L648-L706
public static TriageResult classifyPage(
List<IObject> filteredContents, int pageNumber, TriageThresholds thresholds) {
TriageSignals signals = extractSignals(filteredContents, pageNumber, thresholds);
// Signal 0: CID font extraction failure (highest priority)
if (StaticLayoutContainers.getReplacementCharRatio(pageNumber) >= 0.3)
return TriageResult.backend(pageNumber, 1.0, signals);
// Signal 1: TableBorder presence (most reliable)
if (signals.hasTableBorder())
return TriageResult.backend(pageNumber, 1.0, signals);
// Signal 2: Vector graphics based table detection
if (signals.hasVectorTableSignal())
return TriageResult.backend(pageNumber, 0.95, signals);
// Signal 3: Text-based table patterns
if (signals.hasTextTablePattern())
return TriageResult.backend(pageNumber, 0.9, signals);
// Signal 4: Suspicious text patterns (catches borderless tables)
// Note: Disabled (Experiment 003). Caused 19 FPs (28.4%).
// if (signals.hasSuspiciousPattern()) { ... }
// ...
return TriageResult.java(pageNumber, 0.9, signals);
}The checks run from strongest signal to weakest. The disabled Signal 4 matters because the comment includes a measured false-positive rate of 28.4 percent, which shows the thresholds were tuned against a benchmark rather than guessed. Pages that pass every check stay on the fast Java path.
extractSignals does a single pass over the page objects and sorts each one into a SignalAccumulator.
opendataloader-project/opendataloader-pdf:.../hybrid/TriageProcessor.java:L717-L776
static TriageSignals extractSignals(
List<IObject> filteredContents, int pageNumber, TriageThresholds thresholds) {
if (filteredContents == null || filteredContents.isEmpty()) {
return TriageSignals.empty();
}
SignalAccumulator accumulator = new SignalAccumulator();
for (IObject content : filteredContents) {
if (content instanceof LineChunk) {
accumulator.processLineChunk((LineChunk) content);
} else if (content instanceof TextChunk) {
accumulator.processTextChunk((TextChunk) content);
} else if (content instanceof LineArtChunk) {
accumulator.processLineArtChunk();
} else if (content instanceof ImageChunk) {
accumulator.processImageChunk((ImageChunk) content);
}
}
// Derived values
int totalCount = filteredContents.size();
double lineToTextRatio = totalCount > 0
? (double) accumulator.lineChunkCount / totalCount : 0.0;
boolean hasTableBorder = checkTableBorderPresence(pageNumber);
boolean hasSuspiciousPattern = checkSuspiciousPatterns(accumulator.textChunks);
int alignedLineGroups = countAlignedLineGroups(
accumulator.textChunks, thresholds.getGridGapMultiplier());
boolean hasGridLines = accumulator.horizontalLineCount >= MIN_GRID_LINES
&& accumulator.verticalLineCount >= MIN_GRID_LINES;
boolean hasTableBorderLines = (accumulator.horizontalLineCount + accumulator.verticalLineCount)
>= MIN_LINE_COUNT_FOR_TABLE;
// ...
return new TriageSignals(
accumulator.lineChunkCount, accumulator.textChunkCount, lineToTextRatio,
alignedLineGroups, hasTableBorder, hasSuspiciousPattern,
// ... 13 more fields
);
}The accumulator stores raw counts for lines, text chunks, and images, then derives higher-level booleans from them. hasGridLines requires both horizontal and vertical counts to clear a threshold, while hasTableBorderLines uses their combined count to catch one-direction borders. Those derived signals feed the triage cascade above.

