Merge pull request #1 from aguzererler/claude/heuristic-knuth
feat(cli): three-tier LLM provider setup with dynamic Ollama model listing
This commit is contained in:
commit
7ef012f3ae
136
cli/main.py
136
cli/main.py
|
|
@ -459,10 +459,20 @@ def update_display(layout, spinner_text=None, stats_handler=None, start_time=Non
|
||||||
layout["footer"].update(Panel(stats_table, border_style="grey50"))
|
layout["footer"].update(Panel(stats_table, border_style="grey50"))
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_provider_thinking_config(provider: str):
|
||||||
|
"""Ask for provider-specific thinking config. Returns (thinking_level, reasoning_effort)."""
|
||||||
|
provider_lower = provider.lower()
|
||||||
|
if provider_lower == "google":
|
||||||
|
return ask_gemini_thinking_config(), None
|
||||||
|
elif provider_lower in ("openai", "xai"):
|
||||||
|
return None, ask_openai_reasoning_effort()
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def get_user_selections():
|
def get_user_selections():
|
||||||
"""Get all user selections before starting the analysis display."""
|
"""Get all user selections before starting the analysis display."""
|
||||||
# Display ASCII art welcome message
|
# Display ASCII art welcome message
|
||||||
with open("./cli/static/welcome.txt", "r") as f:
|
with open("./cli/static/welcome.txt", "r", encoding="utf-8") as f:
|
||||||
welcome_ascii = f.read()
|
welcome_ascii = f.read()
|
||||||
|
|
||||||
# Create welcome box content
|
# Create welcome box content
|
||||||
|
|
@ -536,83 +546,65 @@ def get_user_selections():
|
||||||
)
|
)
|
||||||
selected_research_depth = select_research_depth()
|
selected_research_depth = select_research_depth()
|
||||||
|
|
||||||
# Step 5: OpenAI backend
|
# Step 5: Quick-thinking provider + model
|
||||||
console.print(
|
console.print(
|
||||||
create_question_box(
|
create_question_box(
|
||||||
"Step 5: OpenAI backend", "Select which service to talk to"
|
"Step 5: Quick-Thinking Setup",
|
||||||
|
"Provider and model for analysts & risk debaters (fast, high volume)"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
selected_llm_provider, backend_url = select_llm_provider()
|
quick_provider, quick_backend_url = select_llm_provider()
|
||||||
|
selected_shallow_thinker = select_shallow_thinking_agent(quick_provider)
|
||||||
# Step 6: Thinking agents
|
quick_thinking_level, quick_reasoning_effort = _ask_provider_thinking_config(quick_provider)
|
||||||
|
|
||||||
|
# Step 6: Mid-thinking provider + model
|
||||||
console.print(
|
console.print(
|
||||||
create_question_box(
|
create_question_box(
|
||||||
"Step 6: Thinking Agents", "Select your thinking agents for analysis"
|
"Step 6: Mid-Thinking Setup",
|
||||||
|
"Provider and model for researchers & trader (reasoning, argument formation)"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
selected_shallow_thinker = select_shallow_thinking_agent(selected_llm_provider)
|
mid_provider, mid_backend_url = select_llm_provider()
|
||||||
selected_deep_thinker = select_deep_thinking_agent(selected_llm_provider)
|
selected_mid_thinker = select_mid_thinking_agent(mid_provider)
|
||||||
|
mid_thinking_level, mid_reasoning_effort = _ask_provider_thinking_config(mid_provider)
|
||||||
|
|
||||||
# Step 7: Provider-specific thinking configuration
|
# Step 7: Deep-thinking provider + model
|
||||||
thinking_level = None
|
console.print(
|
||||||
reasoning_effort = None
|
create_question_box(
|
||||||
|
"Step 7: Deep-Thinking Setup",
|
||||||
provider_lower = selected_llm_provider.lower()
|
"Provider and model for investment judge & risk manager (final decisions)"
|
||||||
if provider_lower == "google":
|
|
||||||
console.print(
|
|
||||||
create_question_box(
|
|
||||||
"Step 7: Thinking Mode",
|
|
||||||
"Configure Gemini thinking mode"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
thinking_level = ask_gemini_thinking_config()
|
)
|
||||||
elif provider_lower == "openai":
|
deep_provider, deep_backend_url = select_llm_provider()
|
||||||
console.print(
|
selected_deep_thinker = select_deep_thinking_agent(deep_provider)
|
||||||
create_question_box(
|
deep_thinking_level, deep_reasoning_effort = _ask_provider_thinking_config(deep_provider)
|
||||||
"Step 7: Reasoning Effort",
|
|
||||||
"Configure OpenAI reasoning effort level"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
reasoning_effort = ask_openai_reasoning_effort()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ticker": selected_ticker,
|
"ticker": selected_ticker,
|
||||||
"analysis_date": analysis_date,
|
"analysis_date": analysis_date,
|
||||||
"analysts": selected_analysts,
|
"analysts": selected_analysts,
|
||||||
"research_depth": selected_research_depth,
|
"research_depth": selected_research_depth,
|
||||||
"llm_provider": selected_llm_provider.lower(),
|
# Quick
|
||||||
"backend_url": backend_url,
|
"quick_provider": quick_provider.lower(),
|
||||||
|
"quick_backend_url": quick_backend_url,
|
||||||
"shallow_thinker": selected_shallow_thinker,
|
"shallow_thinker": selected_shallow_thinker,
|
||||||
|
"quick_thinking_level": quick_thinking_level,
|
||||||
|
"quick_reasoning_effort": quick_reasoning_effort,
|
||||||
|
# Mid
|
||||||
|
"mid_provider": mid_provider.lower(),
|
||||||
|
"mid_backend_url": mid_backend_url,
|
||||||
|
"mid_thinker": selected_mid_thinker,
|
||||||
|
"mid_thinking_level": mid_thinking_level,
|
||||||
|
"mid_reasoning_effort": mid_reasoning_effort,
|
||||||
|
# Deep
|
||||||
|
"deep_provider": deep_provider.lower(),
|
||||||
|
"deep_backend_url": deep_backend_url,
|
||||||
"deep_thinker": selected_deep_thinker,
|
"deep_thinker": selected_deep_thinker,
|
||||||
"google_thinking_level": thinking_level,
|
"deep_thinking_level": deep_thinking_level,
|
||||||
"openai_reasoning_effort": reasoning_effort,
|
"deep_reasoning_effort": deep_reasoning_effort,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_ticker():
|
|
||||||
"""Get ticker symbol from user input."""
|
|
||||||
return typer.prompt("", default="SPY")
|
|
||||||
|
|
||||||
|
|
||||||
def get_analysis_date():
|
|
||||||
"""Get the analysis date from user input."""
|
|
||||||
while True:
|
|
||||||
date_str = typer.prompt(
|
|
||||||
"", default=datetime.datetime.now().strftime("%Y-%m-%d")
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# Validate date format and ensure it's not in the future
|
|
||||||
analysis_date = datetime.datetime.strptime(date_str, "%Y-%m-%d")
|
|
||||||
if analysis_date.date() > datetime.datetime.now().date():
|
|
||||||
console.print("[red]Error: Analysis date cannot be in the future[/red]")
|
|
||||||
continue
|
|
||||||
return date_str
|
|
||||||
except ValueError:
|
|
||||||
console.print(
|
|
||||||
"[red]Error: Invalid date format. Please use YYYY-MM-DD[/red]"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
def save_report_to_disk(final_state, ticker: str, save_path: Path):
|
||||||
"""Save complete analysis report to disk with organized subfolders."""
|
"""Save complete analysis report to disk with organized subfolders."""
|
||||||
save_path.mkdir(parents=True, exist_ok=True)
|
save_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
@ -904,13 +896,25 @@ def run_analysis():
|
||||||
config = DEFAULT_CONFIG.copy()
|
config = DEFAULT_CONFIG.copy()
|
||||||
config["max_debate_rounds"] = selections["research_depth"]
|
config["max_debate_rounds"] = selections["research_depth"]
|
||||||
config["max_risk_discuss_rounds"] = selections["research_depth"]
|
config["max_risk_discuss_rounds"] = selections["research_depth"]
|
||||||
|
# Per-role LLM configuration
|
||||||
config["quick_think_llm"] = selections["shallow_thinker"]
|
config["quick_think_llm"] = selections["shallow_thinker"]
|
||||||
|
config["quick_think_llm_provider"] = selections["quick_provider"]
|
||||||
|
config["quick_think_backend_url"] = selections["quick_backend_url"]
|
||||||
|
config["quick_think_google_thinking_level"] = selections.get("quick_thinking_level")
|
||||||
|
config["quick_think_openai_reasoning_effort"] = selections.get("quick_reasoning_effort")
|
||||||
|
config["mid_think_llm"] = selections["mid_thinker"]
|
||||||
|
config["mid_think_llm_provider"] = selections["mid_provider"]
|
||||||
|
config["mid_think_backend_url"] = selections["mid_backend_url"]
|
||||||
|
config["mid_think_google_thinking_level"] = selections.get("mid_thinking_level")
|
||||||
|
config["mid_think_openai_reasoning_effort"] = selections.get("mid_reasoning_effort")
|
||||||
config["deep_think_llm"] = selections["deep_thinker"]
|
config["deep_think_llm"] = selections["deep_thinker"]
|
||||||
config["backend_url"] = selections["backend_url"]
|
config["deep_think_llm_provider"] = selections["deep_provider"]
|
||||||
config["llm_provider"] = selections["llm_provider"].lower()
|
config["deep_think_backend_url"] = selections["deep_backend_url"]
|
||||||
# Provider-specific thinking configuration
|
config["deep_think_google_thinking_level"] = selections.get("deep_thinking_level")
|
||||||
config["google_thinking_level"] = selections.get("google_thinking_level")
|
config["deep_think_openai_reasoning_effort"] = selections.get("deep_reasoning_effort")
|
||||||
config["openai_reasoning_effort"] = selections.get("openai_reasoning_effort")
|
# Keep shared llm_provider/backend_url as a fallback (use quick as default)
|
||||||
|
config["llm_provider"] = selections["quick_provider"]
|
||||||
|
config["backend_url"] = selections["quick_backend_url"]
|
||||||
|
|
||||||
# Create stats callback handler for tracking LLM/tool calls
|
# Create stats callback handler for tracking LLM/tool calls
|
||||||
stats_handler = StatsCallbackHandler()
|
stats_handler = StatsCallbackHandler()
|
||||||
|
|
@ -948,10 +952,10 @@ def run_analysis():
|
||||||
func(*args, **kwargs)
|
func(*args, **kwargs)
|
||||||
timestamp, message_type, content = obj.messages[-1]
|
timestamp, message_type, content = obj.messages[-1]
|
||||||
content = content.replace("\n", " ") # Replace newlines with spaces
|
content = content.replace("\n", " ") # Replace newlines with spaces
|
||||||
with open(log_file, "a") as f:
|
with open(log_file, "a", encoding="utf-8") as f:
|
||||||
f.write(f"{timestamp} [{message_type}] {content}\n")
|
f.write(f"{timestamp} [{message_type}] {content}\n")
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
def save_tool_call_decorator(obj, func_name):
|
def save_tool_call_decorator(obj, func_name):
|
||||||
func = getattr(obj, func_name)
|
func = getattr(obj, func_name)
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -959,7 +963,7 @@ def run_analysis():
|
||||||
func(*args, **kwargs)
|
func(*args, **kwargs)
|
||||||
timestamp, tool_name, args = obj.tool_calls[-1]
|
timestamp, tool_name, args = obj.tool_calls[-1]
|
||||||
args_str = ", ".join(f"{k}={v}" for k, v in args.items())
|
args_str = ", ".join(f"{k}={v}" for k, v in args.items())
|
||||||
with open(log_file, "a") as f:
|
with open(log_file, "a", encoding="utf-8") as f:
|
||||||
f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n")
|
f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n")
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
@ -972,7 +976,7 @@ def run_analysis():
|
||||||
content = obj.report_sections[section_name]
|
content = obj.report_sections[section_name]
|
||||||
if content:
|
if content:
|
||||||
file_name = f"{section_name}.md"
|
file_name = f"{section_name}.md"
|
||||||
with open(report_dir / file_name, "w") as f:
|
with open(report_dir / file_name, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
|
||||||
256
cli/utils.py
256
cli/utils.py
|
|
@ -1,8 +1,24 @@
|
||||||
import questionary
|
import questionary
|
||||||
|
import requests
|
||||||
from typing import List, Optional, Tuple, Dict
|
from typing import List, Optional, Tuple, Dict
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
from cli.models import AnalystType
|
from cli.models import AnalystType
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_ollama_models(base_url: str = "http://localhost:11434") -> list[tuple[str, str]]:
|
||||||
|
"""Fetch available models from a running Ollama instance."""
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{base_url}/api/tags", timeout=5)
|
||||||
|
resp.raise_for_status()
|
||||||
|
models = resp.json().get("models", [])
|
||||||
|
return [(m["name"], m["name"]) for m in models] if models else []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
ANALYST_ORDER = [
|
ANALYST_ORDER = [
|
||||||
("Market Analyst", AnalystType.MARKET),
|
("Market Analyst", AnalystType.MARKET),
|
||||||
("Social Media Analyst", AnalystType.SOCIAL),
|
("Social Media Analyst", AnalystType.SOCIAL),
|
||||||
|
|
@ -125,48 +141,56 @@ def select_research_depth() -> int:
|
||||||
def select_shallow_thinking_agent(provider) -> str:
|
def select_shallow_thinking_agent(provider) -> str:
|
||||||
"""Select shallow thinking llm engine using an interactive selection."""
|
"""Select shallow thinking llm engine using an interactive selection."""
|
||||||
|
|
||||||
# Define shallow thinking llm engine options with their corresponding model names
|
_provider = provider.lower()
|
||||||
SHALLOW_AGENT_OPTIONS = {
|
|
||||||
"openai": [
|
if _provider == "ollama":
|
||||||
("GPT-5 Mini - Cost-optimized reasoning", "gpt-5-mini"),
|
ollama_models = _fetch_ollama_models()
|
||||||
("GPT-5 Nano - Ultra-fast, high-throughput", "gpt-5-nano"),
|
if not ollama_models:
|
||||||
("GPT-5.2 - Latest flagship", "gpt-5.2"),
|
console.print("[yellow]Could not reach Ollama — is it running? Enter a model name manually.[/yellow]")
|
||||||
("GPT-5.1 - Flexible reasoning", "gpt-5.1"),
|
model = questionary.text("Model name (e.g. qwen3.5:9b):").ask()
|
||||||
("GPT-4.1 - Smartest non-reasoning, 1M context", "gpt-4.1"),
|
if not model:
|
||||||
],
|
console.print("\n[red]No model entered. Exiting...[/red]")
|
||||||
"anthropic": [
|
exit(1)
|
||||||
("Claude Haiku 4.5 - Fast + extended thinking", "claude-haiku-4-5"),
|
return model.strip()
|
||||||
("Claude Sonnet 4.5 - Best for agents/coding", "claude-sonnet-4-5"),
|
options = ollama_models
|
||||||
("Claude Sonnet 4 - High-performance", "claude-sonnet-4-20250514"),
|
else:
|
||||||
],
|
SHALLOW_AGENT_OPTIONS = {
|
||||||
"google": [
|
"openai": [
|
||||||
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
("GPT-5 Mini - Cost-optimized reasoning", "gpt-5-mini"),
|
||||||
("Gemini 2.5 Flash - Balanced, recommended", "gemini-2.5-flash"),
|
("GPT-5 Nano - Ultra-fast, high-throughput", "gpt-5-nano"),
|
||||||
("Gemini 3 Pro - Reasoning-first", "gemini-3-pro-preview"),
|
("GPT-5.2 - Latest flagship", "gpt-5.2"),
|
||||||
("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"),
|
("GPT-5.1 - Flexible reasoning", "gpt-5.1"),
|
||||||
],
|
("GPT-4.1 - Smartest non-reasoning, 1M context", "gpt-4.1"),
|
||||||
"xai": [
|
],
|
||||||
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
"anthropic": [
|
||||||
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
|
("Claude Haiku 4.5 - Fast + extended thinking", "claude-haiku-4-5"),
|
||||||
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
("Claude Sonnet 4.5 - Best for agents/coding", "claude-sonnet-4-5"),
|
||||||
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
("Claude Sonnet 4 - High-performance", "claude-sonnet-4-20250514"),
|
||||||
],
|
],
|
||||||
"openrouter": [
|
"google": [
|
||||||
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||||
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
("Gemini 2.5 Flash - Balanced, recommended", "gemini-2.5-flash"),
|
||||||
],
|
("Gemini 3 Pro - Reasoning-first", "gemini-3-pro-preview"),
|
||||||
"ollama": [
|
("Gemini 2.5 Flash Lite - Fast, low-cost", "gemini-2.5-flash-lite"),
|
||||||
("Qwen3:latest (8B, local)", "qwen3:latest"),
|
],
|
||||||
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
|
"xai": [
|
||||||
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
|
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||||
],
|
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
|
||||||
}
|
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||||
|
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
||||||
|
],
|
||||||
|
"openrouter": [
|
||||||
|
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
||||||
|
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
options = SHALLOW_AGENT_OPTIONS[_provider]
|
||||||
|
|
||||||
choice = questionary.select(
|
choice = questionary.select(
|
||||||
"Select Your [Quick-Thinking LLM Engine]:",
|
"Select Your [Quick-Thinking LLM Engine]:",
|
||||||
choices=[
|
choices=[
|
||||||
questionary.Choice(display, value=value)
|
questionary.Choice(display, value=value)
|
||||||
for display, value in SHALLOW_AGENT_OPTIONS[provider.lower()]
|
for display, value in options
|
||||||
],
|
],
|
||||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||||
style=questionary.Style(
|
style=questionary.Style(
|
||||||
|
|
@ -187,54 +211,132 @@ def select_shallow_thinking_agent(provider) -> str:
|
||||||
return choice
|
return choice
|
||||||
|
|
||||||
|
|
||||||
|
def select_mid_thinking_agent(provider) -> str:
|
||||||
|
"""Select mid thinking llm engine using an interactive selection."""
|
||||||
|
|
||||||
|
_provider = provider.lower()
|
||||||
|
|
||||||
|
if _provider == "ollama":
|
||||||
|
ollama_models = _fetch_ollama_models()
|
||||||
|
if not ollama_models:
|
||||||
|
console.print("[yellow]Could not reach Ollama — is it running? Enter a model name manually.[/yellow]")
|
||||||
|
model = questionary.text("Model name (e.g. qwen3.5:27b):").ask()
|
||||||
|
if not model:
|
||||||
|
console.print("\n[red]No model entered. Exiting...[/red]")
|
||||||
|
exit(1)
|
||||||
|
return model.strip()
|
||||||
|
options = ollama_models
|
||||||
|
else:
|
||||||
|
MID_AGENT_OPTIONS = {
|
||||||
|
"openai": [
|
||||||
|
("GPT-5.1 - Flexible reasoning", "gpt-5.1"),
|
||||||
|
("GPT-5 - Advanced reasoning", "gpt-5"),
|
||||||
|
("GPT-4.1 - Smartest non-reasoning, 1M context", "gpt-4.1"),
|
||||||
|
("GPT-5 Mini - Cost-optimized reasoning", "gpt-5-mini"),
|
||||||
|
],
|
||||||
|
"anthropic": [
|
||||||
|
("Claude Sonnet 4.5 - Best for agents/coding", "claude-sonnet-4-5"),
|
||||||
|
("Claude Sonnet 4 - High-performance", "claude-sonnet-4-20250514"),
|
||||||
|
("Claude Haiku 4.5 - Fast + extended thinking", "claude-haiku-4-5"),
|
||||||
|
],
|
||||||
|
"google": [
|
||||||
|
("Gemini 2.5 Flash - Balanced, recommended", "gemini-2.5-flash"),
|
||||||
|
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||||
|
("Gemini 3 Pro - Reasoning-first", "gemini-3-pro-preview"),
|
||||||
|
],
|
||||||
|
"xai": [
|
||||||
|
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||||
|
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
||||||
|
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||||
|
],
|
||||||
|
"openrouter": [
|
||||||
|
("DeepSeek R1 - Strong open-source reasoning", "deepseek/deepseek-r1"),
|
||||||
|
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
||||||
|
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
options = MID_AGENT_OPTIONS[_provider]
|
||||||
|
|
||||||
|
choice = questionary.select(
|
||||||
|
"Select Your [Mid-Thinking LLM Engine]:",
|
||||||
|
choices=[
|
||||||
|
questionary.Choice(display, value=value)
|
||||||
|
for display, value in options
|
||||||
|
],
|
||||||
|
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||||
|
style=questionary.Style(
|
||||||
|
[
|
||||||
|
("selected", "fg:magenta noinherit"),
|
||||||
|
("highlighted", "fg:magenta noinherit"),
|
||||||
|
("pointer", "fg:magenta noinherit"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
).ask()
|
||||||
|
|
||||||
|
if choice is None:
|
||||||
|
console.print("\n[red]No mid thinking llm engine selected. Exiting...[/red]")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
return choice
|
||||||
|
|
||||||
|
|
||||||
def select_deep_thinking_agent(provider) -> str:
|
def select_deep_thinking_agent(provider) -> str:
|
||||||
"""Select deep thinking llm engine using an interactive selection."""
|
"""Select deep thinking llm engine using an interactive selection."""
|
||||||
|
|
||||||
# Define deep thinking llm engine options with their corresponding model names
|
_provider = provider.lower()
|
||||||
DEEP_AGENT_OPTIONS = {
|
|
||||||
"openai": [
|
if _provider == "ollama":
|
||||||
("GPT-5.2 - Latest flagship", "gpt-5.2"),
|
ollama_models = _fetch_ollama_models()
|
||||||
("GPT-5.1 - Flexible reasoning", "gpt-5.1"),
|
if not ollama_models:
|
||||||
("GPT-5 - Advanced reasoning", "gpt-5"),
|
console.print("[yellow]Could not reach Ollama — is it running? Enter a model name manually.[/yellow]")
|
||||||
("GPT-4.1 - Smartest non-reasoning, 1M context", "gpt-4.1"),
|
model = questionary.text("Model name (e.g. qwen3.5:27b):").ask()
|
||||||
("GPT-5 Mini - Cost-optimized reasoning", "gpt-5-mini"),
|
if not model:
|
||||||
("GPT-5 Nano - Ultra-fast, high-throughput", "gpt-5-nano"),
|
console.print("\n[red]No model entered. Exiting...[/red]")
|
||||||
],
|
exit(1)
|
||||||
"anthropic": [
|
return model.strip()
|
||||||
("Claude Sonnet 4.5 - Best for agents/coding", "claude-sonnet-4-5"),
|
options = ollama_models
|
||||||
("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"),
|
else:
|
||||||
("Claude Opus 4.1 - Most capable model", "claude-opus-4-1-20250805"),
|
DEEP_AGENT_OPTIONS = {
|
||||||
("Claude Haiku 4.5 - Fast + extended thinking", "claude-haiku-4-5"),
|
"openai": [
|
||||||
("Claude Sonnet 4 - High-performance", "claude-sonnet-4-20250514"),
|
("GPT-5.2 - Latest flagship", "gpt-5.2"),
|
||||||
],
|
("GPT-5.1 - Flexible reasoning", "gpt-5.1"),
|
||||||
"google": [
|
("GPT-5 - Advanced reasoning", "gpt-5"),
|
||||||
("Gemini 3 Pro - Reasoning-first", "gemini-3-pro-preview"),
|
("GPT-4.1 - Smartest non-reasoning, 1M context", "gpt-4.1"),
|
||||||
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
("GPT-5 Mini - Cost-optimized reasoning", "gpt-5-mini"),
|
||||||
("Gemini 2.5 Flash - Balanced, recommended", "gemini-2.5-flash"),
|
("GPT-5 Nano - Ultra-fast, high-throughput", "gpt-5-nano"),
|
||||||
],
|
],
|
||||||
"xai": [
|
"anthropic": [
|
||||||
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
("Claude Sonnet 4.5 - Best for agents/coding", "claude-sonnet-4-5"),
|
||||||
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
("Claude Opus 4.5 - Premium, max intelligence", "claude-opus-4-5"),
|
||||||
("Grok 4 - Flagship model", "grok-4-0709"),
|
("Claude Opus 4.1 - Most capable model", "claude-opus-4-1-20250805"),
|
||||||
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
("Claude Haiku 4.5 - Fast + extended thinking", "claude-haiku-4-5"),
|
||||||
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
|
("Claude Sonnet 4 - High-performance", "claude-sonnet-4-20250514"),
|
||||||
],
|
],
|
||||||
"openrouter": [
|
"google": [
|
||||||
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
("Gemini 3 Pro - Reasoning-first", "gemini-3-pro-preview"),
|
||||||
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
("Gemini 3 Flash - Next-gen fast", "gemini-3-flash-preview"),
|
||||||
],
|
("Gemini 2.5 Flash - Balanced, recommended", "gemini-2.5-flash"),
|
||||||
"ollama": [
|
],
|
||||||
("GLM-4.7-Flash:latest (30B, local)", "glm-4.7-flash:latest"),
|
"xai": [
|
||||||
("GPT-OSS:latest (20B, local)", "gpt-oss:latest"),
|
("Grok 4.1 Fast (Reasoning) - High-performance, 2M ctx", "grok-4-1-fast-reasoning"),
|
||||||
("Qwen3:latest (8B, local)", "qwen3:latest"),
|
("Grok 4 Fast (Reasoning) - High-performance", "grok-4-fast-reasoning"),
|
||||||
],
|
("Grok 4 - Flagship model", "grok-4-0709"),
|
||||||
}
|
("Grok 4.1 Fast (Non-Reasoning) - Speed optimized, 2M ctx", "grok-4-1-fast-non-reasoning"),
|
||||||
|
("Grok 4 Fast (Non-Reasoning) - Speed optimized", "grok-4-fast-non-reasoning"),
|
||||||
|
],
|
||||||
|
"openrouter": [
|
||||||
|
("DeepSeek R1 - Strong open-source reasoning", "deepseek/deepseek-r1"),
|
||||||
|
("Z.AI GLM 4.5 Air (free)", "z-ai/glm-4.5-air:free"),
|
||||||
|
("NVIDIA Nemotron 3 Nano 30B (free)", "nvidia/nemotron-3-nano-30b-a3b:free"),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
options = DEEP_AGENT_OPTIONS[_provider]
|
||||||
|
|
||||||
choice = questionary.select(
|
choice = questionary.select(
|
||||||
"Select Your [Deep-Thinking LLM Engine]:",
|
"Select Your [Deep-Thinking LLM Engine]:",
|
||||||
choices=[
|
choices=[
|
||||||
questionary.Choice(display, value=value)
|
questionary.Choice(display, value=value)
|
||||||
for display, value in DEEP_AGENT_OPTIONS[provider.lower()]
|
for display, value in options
|
||||||
],
|
],
|
||||||
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
|
||||||
style=questionary.Style(
|
style=questionary.Style(
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,26 @@ DEFAULT_CONFIG = {
|
||||||
# LLM settings
|
# LLM settings
|
||||||
"llm_provider": "openai",
|
"llm_provider": "openai",
|
||||||
"deep_think_llm": "gpt-5.2",
|
"deep_think_llm": "gpt-5.2",
|
||||||
|
"mid_think_llm": None, # falls back to quick_think_llm when None
|
||||||
"quick_think_llm": "gpt-5-mini",
|
"quick_think_llm": "gpt-5-mini",
|
||||||
"backend_url": "https://api.openai.com/v1",
|
"backend_url": "https://api.openai.com/v1",
|
||||||
# Provider-specific thinking configuration
|
# Per-role provider overrides (fall back to llm_provider / backend_url when None)
|
||||||
|
"deep_think_llm_provider": None, # e.g. "google", "anthropic", "openai"
|
||||||
|
"deep_think_backend_url": None, # override backend URL for deep-think model
|
||||||
|
"mid_think_llm_provider": None, # e.g. "ollama"
|
||||||
|
"mid_think_backend_url": None, # override backend URL for mid-think model
|
||||||
|
"quick_think_llm_provider": None, # e.g. "openai", "ollama"
|
||||||
|
"quick_think_backend_url": None, # override backend URL for quick-think model
|
||||||
|
# Provider-specific thinking configuration (applies to all roles unless overridden)
|
||||||
"google_thinking_level": None, # "high", "minimal", etc.
|
"google_thinking_level": None, # "high", "minimal", etc.
|
||||||
"openai_reasoning_effort": None, # "medium", "high", "low"
|
"openai_reasoning_effort": None, # "medium", "high", "low"
|
||||||
|
# Per-role provider-specific thinking configuration
|
||||||
|
"deep_think_google_thinking_level": None,
|
||||||
|
"deep_think_openai_reasoning_effort": None,
|
||||||
|
"mid_think_google_thinking_level": None,
|
||||||
|
"mid_think_openai_reasoning_effort": None,
|
||||||
|
"quick_think_google_thinking_level": None,
|
||||||
|
"quick_think_openai_reasoning_effort": None,
|
||||||
# Debate and discussion settings
|
# Debate and discussion settings
|
||||||
"max_debate_rounds": 1,
|
"max_debate_rounds": 1,
|
||||||
"max_risk_discuss_rounds": 1,
|
"max_risk_discuss_rounds": 1,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class GraphSetup:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
quick_thinking_llm: ChatOpenAI,
|
quick_thinking_llm: ChatOpenAI,
|
||||||
|
mid_thinking_llm: ChatOpenAI,
|
||||||
deep_thinking_llm: ChatOpenAI,
|
deep_thinking_llm: ChatOpenAI,
|
||||||
tool_nodes: Dict[str, ToolNode],
|
tool_nodes: Dict[str, ToolNode],
|
||||||
bull_memory,
|
bull_memory,
|
||||||
|
|
@ -28,6 +29,7 @@ class GraphSetup:
|
||||||
):
|
):
|
||||||
"""Initialize with required components."""
|
"""Initialize with required components."""
|
||||||
self.quick_thinking_llm = quick_thinking_llm
|
self.quick_thinking_llm = quick_thinking_llm
|
||||||
|
self.mid_thinking_llm = mid_thinking_llm
|
||||||
self.deep_thinking_llm = deep_thinking_llm
|
self.deep_thinking_llm = deep_thinking_llm
|
||||||
self.tool_nodes = tool_nodes
|
self.tool_nodes = tool_nodes
|
||||||
self.bull_memory = bull_memory
|
self.bull_memory = bull_memory
|
||||||
|
|
@ -87,15 +89,15 @@ class GraphSetup:
|
||||||
|
|
||||||
# Create researcher and manager nodes
|
# Create researcher and manager nodes
|
||||||
bull_researcher_node = create_bull_researcher(
|
bull_researcher_node = create_bull_researcher(
|
||||||
self.quick_thinking_llm, self.bull_memory
|
self.mid_thinking_llm, self.bull_memory
|
||||||
)
|
)
|
||||||
bear_researcher_node = create_bear_researcher(
|
bear_researcher_node = create_bear_researcher(
|
||||||
self.quick_thinking_llm, self.bear_memory
|
self.mid_thinking_llm, self.bear_memory
|
||||||
)
|
)
|
||||||
research_manager_node = create_research_manager(
|
research_manager_node = create_research_manager(
|
||||||
self.deep_thinking_llm, self.invest_judge_memory
|
self.deep_thinking_llm, self.invest_judge_memory
|
||||||
)
|
)
|
||||||
trader_node = create_trader(self.quick_thinking_llm, self.trader_memory)
|
trader_node = create_trader(self.mid_thinking_llm, self.trader_memory)
|
||||||
|
|
||||||
# Create risk analysis nodes
|
# Create risk analysis nodes
|
||||||
aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm)
|
aggressive_analyst = create_aggressive_debator(self.quick_thinking_llm)
|
||||||
|
|
|
||||||
|
|
@ -71,27 +71,65 @@ class TradingAgentsGraph:
|
||||||
exist_ok=True,
|
exist_ok=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize LLMs with provider-specific thinking configuration
|
# Initialize LLMs with provider-specific thinking configuration.
|
||||||
llm_kwargs = self._get_provider_kwargs()
|
# Per-role provider/backend_url keys take precedence over the shared ones.
|
||||||
|
deep_kwargs = self._get_provider_kwargs("deep_think")
|
||||||
|
mid_kwargs = self._get_provider_kwargs("mid_think")
|
||||||
|
quick_kwargs = self._get_provider_kwargs("quick_think")
|
||||||
|
|
||||||
# Add callbacks to kwargs if provided (passed to LLM constructor)
|
# Add callbacks to kwargs if provided (passed to LLM constructor)
|
||||||
if self.callbacks:
|
if self.callbacks:
|
||||||
llm_kwargs["callbacks"] = self.callbacks
|
deep_kwargs["callbacks"] = self.callbacks
|
||||||
|
mid_kwargs["callbacks"] = self.callbacks
|
||||||
|
quick_kwargs["callbacks"] = self.callbacks
|
||||||
|
|
||||||
|
deep_provider = (
|
||||||
|
self.config.get("deep_think_llm_provider") or self.config["llm_provider"]
|
||||||
|
)
|
||||||
|
deep_backend_url = (
|
||||||
|
self.config.get("deep_think_backend_url") or self.config.get("backend_url")
|
||||||
|
)
|
||||||
|
quick_provider = (
|
||||||
|
self.config.get("quick_think_llm_provider") or self.config["llm_provider"]
|
||||||
|
)
|
||||||
|
quick_backend_url = (
|
||||||
|
self.config.get("quick_think_backend_url") or self.config.get("backend_url")
|
||||||
|
)
|
||||||
|
|
||||||
|
# mid_think falls back to quick_think when not configured
|
||||||
|
mid_model = self.config.get("mid_think_llm") or self.config["quick_think_llm"]
|
||||||
|
mid_provider = (
|
||||||
|
self.config.get("mid_think_llm_provider")
|
||||||
|
or self.config.get("quick_think_llm_provider")
|
||||||
|
or self.config["llm_provider"]
|
||||||
|
)
|
||||||
|
mid_backend_url = (
|
||||||
|
self.config.get("mid_think_backend_url")
|
||||||
|
or self.config.get("quick_think_backend_url")
|
||||||
|
or self.config.get("backend_url")
|
||||||
|
)
|
||||||
|
|
||||||
deep_client = create_llm_client(
|
deep_client = create_llm_client(
|
||||||
provider=self.config["llm_provider"],
|
provider=deep_provider,
|
||||||
model=self.config["deep_think_llm"],
|
model=self.config["deep_think_llm"],
|
||||||
base_url=self.config.get("backend_url"),
|
base_url=deep_backend_url,
|
||||||
**llm_kwargs,
|
**deep_kwargs,
|
||||||
|
)
|
||||||
|
mid_client = create_llm_client(
|
||||||
|
provider=mid_provider,
|
||||||
|
model=mid_model,
|
||||||
|
base_url=mid_backend_url,
|
||||||
|
**mid_kwargs,
|
||||||
)
|
)
|
||||||
quick_client = create_llm_client(
|
quick_client = create_llm_client(
|
||||||
provider=self.config["llm_provider"],
|
provider=quick_provider,
|
||||||
model=self.config["quick_think_llm"],
|
model=self.config["quick_think_llm"],
|
||||||
base_url=self.config.get("backend_url"),
|
base_url=quick_backend_url,
|
||||||
**llm_kwargs,
|
**quick_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.deep_thinking_llm = deep_client.get_llm()
|
self.deep_thinking_llm = deep_client.get_llm()
|
||||||
|
self.mid_thinking_llm = mid_client.get_llm()
|
||||||
self.quick_thinking_llm = quick_client.get_llm()
|
self.quick_thinking_llm = quick_client.get_llm()
|
||||||
|
|
||||||
# Initialize memories
|
# Initialize memories
|
||||||
|
|
@ -108,6 +146,7 @@ class TradingAgentsGraph:
|
||||||
self.conditional_logic = ConditionalLogic()
|
self.conditional_logic = ConditionalLogic()
|
||||||
self.graph_setup = GraphSetup(
|
self.graph_setup = GraphSetup(
|
||||||
self.quick_thinking_llm,
|
self.quick_thinking_llm,
|
||||||
|
self.mid_thinking_llm,
|
||||||
self.deep_thinking_llm,
|
self.deep_thinking_llm,
|
||||||
self.tool_nodes,
|
self.tool_nodes,
|
||||||
self.bull_memory,
|
self.bull_memory,
|
||||||
|
|
@ -130,18 +169,33 @@ class TradingAgentsGraph:
|
||||||
# Set up the graph
|
# Set up the graph
|
||||||
self.graph = self.graph_setup.setup_graph(selected_analysts)
|
self.graph = self.graph_setup.setup_graph(selected_analysts)
|
||||||
|
|
||||||
def _get_provider_kwargs(self) -> Dict[str, Any]:
|
def _get_provider_kwargs(self, role: str = "") -> Dict[str, Any]:
|
||||||
"""Get provider-specific kwargs for LLM client creation."""
|
"""Get provider-specific kwargs for LLM client creation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
role: Either "deep_think" or "quick_think". When provided the
|
||||||
|
per-role config keys take precedence over the shared keys.
|
||||||
|
"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
provider = self.config.get("llm_provider", "").lower()
|
prefix = f"{role}_" if role else ""
|
||||||
|
provider = (
|
||||||
|
self.config.get(f"{prefix}llm_provider")
|
||||||
|
or self.config.get("llm_provider", "")
|
||||||
|
).lower()
|
||||||
|
|
||||||
if provider == "google":
|
if provider == "google":
|
||||||
thinking_level = self.config.get("google_thinking_level")
|
thinking_level = (
|
||||||
|
self.config.get(f"{prefix}google_thinking_level")
|
||||||
|
or self.config.get("google_thinking_level")
|
||||||
|
)
|
||||||
if thinking_level:
|
if thinking_level:
|
||||||
kwargs["thinking_level"] = thinking_level
|
kwargs["thinking_level"] = thinking_level
|
||||||
|
|
||||||
elif provider == "openai":
|
elif provider in ("openai", "xai", "openrouter", "ollama"):
|
||||||
reasoning_effort = self.config.get("openai_reasoning_effort")
|
reasoning_effort = (
|
||||||
|
self.config.get(f"{prefix}openai_reasoning_effort")
|
||||||
|
or self.config.get("openai_reasoning_effort")
|
||||||
|
)
|
||||||
if reasoning_effort:
|
if reasoning_effort:
|
||||||
kwargs["reasoning_effort"] = reasoning_effort
|
kwargs["reasoning_effort"] = reasoning_effort
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue