headless operation
This commit is contained in:
parent
84a43853c4
commit
2fa430442a
|
|
@ -1 +1 @@
|
||||||
3.10
|
3.11
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Headless Mode Usage Guide
|
||||||
|
|
||||||
|
TradingAgents now supports headless operation for automated trading analysis without interactive prompts.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Basic headless analysis
|
||||||
|
python -m cli.main --ticker TSLA --headless
|
||||||
|
|
||||||
|
# Full configuration example
|
||||||
|
python -m cli.main \
|
||||||
|
--ticker TSLA \
|
||||||
|
--date 2024-05-10 \
|
||||||
|
--analysts market,social,news,fundamentals \
|
||||||
|
--depth deep \
|
||||||
|
--llm-provider openai \
|
||||||
|
--shallow-model gpt-4o-mini \
|
||||||
|
--deep-model o1-mini \
|
||||||
|
--output-dir ./reports \
|
||||||
|
--headless \
|
||||||
|
--quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command-Line Options
|
||||||
|
|
||||||
|
| Option | Short | Description | Default |
|
||||||
|
|--------|-------|-------------|---------|
|
||||||
|
| `--ticker` | `-t` | Stock ticker symbol (required in headless mode) | None |
|
||||||
|
| `--date` | `-d` | Analysis date (YYYY-MM-DD) | Today's date |
|
||||||
|
| `--analysts` | `-a` | Comma-separated analysts list | `market,social,news,fundamentals` |
|
||||||
|
| `--depth` | | Research depth (`shallow`, `medium`, `deep`) | `medium` |
|
||||||
|
| `--llm-provider` | | LLM provider (`openai`, `anthropic`, `google`) | `openai` |
|
||||||
|
| `--backend-url` | | LLM backend URL | `https://api.openai.com/v1` |
|
||||||
|
| `--shallow-model` | | Model for quick thinking | `gpt-4o-mini` |
|
||||||
|
| `--deep-model` | | Model for deep thinking | `o1-mini` |
|
||||||
|
| `--output-dir` | `-o` | Output directory for reports | `./results` |
|
||||||
|
| `--config` | `-c` | Configuration file path | None |
|
||||||
|
| `--headless` | | Enable headless mode | Interactive mode |
|
||||||
|
| `--quiet` | `-q` | Suppress verbose output | Verbose output |
|
||||||
|
|
||||||
|
## Configuration File
|
||||||
|
|
||||||
|
Create a JSON configuration file for complex setups:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"llm_provider": "openai",
|
||||||
|
"deep_think_llm": "o1-mini",
|
||||||
|
"quick_think_llm": "gpt-4o-mini",
|
||||||
|
"backend_url": "https://api.openai.com/v1",
|
||||||
|
"max_debate_rounds": 3,
|
||||||
|
"max_risk_discuss_rounds": 3,
|
||||||
|
"online_tools": true,
|
||||||
|
"results_dir": "./results"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use with: `python -m cli.main --config config.json --ticker AAPL --headless`
|
||||||
|
|
||||||
|
## Output Structure
|
||||||
|
|
||||||
|
Reports are saved in: `{output_dir}/{ticker}/{date}/`
|
||||||
|
|
||||||
|
- `reports/` - Individual analyst reports as markdown files
|
||||||
|
- `complete_report.md` - Combined analysis report
|
||||||
|
- `decision.json` - Final trading decision in JSON format
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Automated Pipeline
|
||||||
|
```bash
|
||||||
|
# Run daily analysis for multiple stocks
|
||||||
|
for ticker in AAPL TSLA NVDA; do
|
||||||
|
python -m cli.main --ticker $ticker --headless --quiet > results_$ticker.json
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### CI/CD Integration
|
||||||
|
```bash
|
||||||
|
# Quick analysis for deployment
|
||||||
|
python -m cli.main \
|
||||||
|
--ticker SPY \
|
||||||
|
--analysts market,news \
|
||||||
|
--depth shallow \
|
||||||
|
--headless \
|
||||||
|
--quiet
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Analysis
|
||||||
|
```bash
|
||||||
|
# Deep research with custom models
|
||||||
|
python -m cli.main \
|
||||||
|
--ticker MSFT \
|
||||||
|
--depth deep \
|
||||||
|
--llm-provider anthropic \
|
||||||
|
--shallow-model claude-3-5-haiku-latest \
|
||||||
|
--deep-model claude-sonnet-4-0 \
|
||||||
|
--headless
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- Missing `--ticker` in headless mode: Command will fail with clear error message
|
||||||
|
- Invalid analyst names: Command will list valid options
|
||||||
|
- Invalid date format: Must be YYYY-MM-DD format
|
||||||
|
- Missing API keys: Will fail during analysis (set environment variables)
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required for analysis:
|
||||||
|
```bash
|
||||||
|
export OPENAI_API_KEY=your_openai_key
|
||||||
|
export FINNHUB_API_KEY=your_finnhub_key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Scripts
|
||||||
|
|
||||||
|
The headless mode is designed for:
|
||||||
|
- Automated trading systems
|
||||||
|
- Batch processing workflows
|
||||||
|
- CI/CD pipelines
|
||||||
|
- Scheduled analysis jobs
|
||||||
|
- API integrations
|
||||||
|
|
||||||
|
Return codes:
|
||||||
|
- 0: Success
|
||||||
|
- 1: Error (invalid arguments, missing keys, etc.)
|
||||||
252
cli/main.py
252
cli/main.py
|
|
@ -1,8 +1,9 @@
|
||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
import datetime
|
import datetime
|
||||||
import typer
|
import typer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
import json
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.spinner import Spinner
|
from rich.spinner import Spinner
|
||||||
|
|
@ -11,19 +12,22 @@ from rich.columns import Columns
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
from rich.layout import Layout
|
from rich.layout import Layout
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.live import Live
|
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from collections import deque
|
from collections import deque
|
||||||
import time
|
|
||||||
from rich.tree import Tree
|
|
||||||
from rich import box
|
from rich import box
|
||||||
from rich.align import Align
|
from rich.align import Align
|
||||||
from rich.rule import Rule
|
|
||||||
|
|
||||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
from tradingagents.default_config import DEFAULT_CONFIG
|
from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
from cli.models import AnalystType
|
from cli.utils import (
|
||||||
from cli.utils import *
|
get_ticker as prompt_ticker,
|
||||||
|
get_analysis_date as prompt_analysis_date,
|
||||||
|
select_analysts,
|
||||||
|
select_research_depth,
|
||||||
|
select_llm_provider,
|
||||||
|
select_shallow_thinking_agent,
|
||||||
|
select_deep_thinking_agent,
|
||||||
|
)
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
@ -31,6 +35,7 @@ app = typer.Typer(
|
||||||
name="TradingAgents",
|
name="TradingAgents",
|
||||||
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework",
|
||||||
add_completion=True, # Enable shell completion
|
add_completion=True, # Enable shell completion
|
||||||
|
no_args_is_help=False, # Don't show help when no args provided
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -338,10 +343,10 @@ def update_display(layout, spinner_text=None):
|
||||||
if spinner_text:
|
if spinner_text:
|
||||||
messages_table.add_row("", "Spinner", spinner_text)
|
messages_table.add_row("", "Spinner", spinner_text)
|
||||||
|
|
||||||
# Add a footer to indicate if messages were truncated
|
# Add a row to indicate if messages were truncated
|
||||||
if len(all_messages) > max_messages:
|
if len(all_messages) > max_messages:
|
||||||
messages_table.footer = (
|
messages_table.add_row(
|
||||||
f"[dim]Showing last {max_messages} of {len(all_messages)} messages[/dim]"
|
"", "Info", f"[dim]Showing last {max_messages} of {len(all_messages)} messages[/dim]"
|
||||||
)
|
)
|
||||||
|
|
||||||
layout["messages"].update(
|
layout["messages"].update(
|
||||||
|
|
@ -431,7 +436,7 @@ def get_user_selections():
|
||||||
"Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
|
"Step 1: Ticker Symbol", "Enter the ticker symbol to analyze", "SPY"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
selected_ticker = get_ticker()
|
selected_ticker = prompt_ticker()
|
||||||
|
|
||||||
# Step 2: Analysis date
|
# Step 2: Analysis date
|
||||||
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
default_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
@ -442,7 +447,7 @@ def get_user_selections():
|
||||||
default_date,
|
default_date,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
analysis_date = get_analysis_date()
|
analysis_date = prompt_analysis_date()
|
||||||
|
|
||||||
# Step 3: Select analysts
|
# Step 3: Select analysts
|
||||||
console.print(
|
console.print(
|
||||||
|
|
@ -492,28 +497,6 @@ def get_user_selections():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 display_complete_report(final_state):
|
def display_complete_report(final_state):
|
||||||
|
|
@ -799,7 +782,7 @@ def run_analysis():
|
||||||
# Now start the display layout
|
# Now start the display layout
|
||||||
layout = create_layout()
|
layout = create_layout()
|
||||||
|
|
||||||
with Live(layout, refresh_per_second=4) as live:
|
with Live(layout, refresh_per_second=4):
|
||||||
# Initial display
|
# Initial display
|
||||||
update_display(layout)
|
update_display(layout)
|
||||||
|
|
||||||
|
|
@ -1073,9 +1056,9 @@ def run_analysis():
|
||||||
|
|
||||||
trace.append(chunk)
|
trace.append(chunk)
|
||||||
|
|
||||||
# Get final state and decision
|
# Get final state
|
||||||
final_state = trace[-1]
|
final_state = trace[-1]
|
||||||
decision = graph.process_signal(final_state["final_trade_decision"])
|
graph.process_signal(final_state["final_trade_decision"])
|
||||||
|
|
||||||
# Update all agent statuses to completed
|
# Update all agent statuses to completed
|
||||||
for agent in message_buffer.agent_status:
|
for agent in message_buffer.agent_status:
|
||||||
|
|
@ -1096,10 +1079,197 @@ def run_analysis():
|
||||||
update_display(layout)
|
update_display(layout)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
def run_headless_analysis(
|
||||||
def analyze():
|
ticker: str,
|
||||||
run_analysis()
|
analysis_date: str,
|
||||||
|
analysts: List[str],
|
||||||
|
research_depth: int,
|
||||||
|
llm_provider: str,
|
||||||
|
backend_url: str,
|
||||||
|
shallow_thinker: str,
|
||||||
|
deep_thinker: str,
|
||||||
|
output_dir: str,
|
||||||
|
quiet: bool = False,
|
||||||
|
config_file: Optional[str] = None,
|
||||||
|
):
|
||||||
|
"""Run analysis in headless mode without interactive prompts."""
|
||||||
|
|
||||||
|
# Load config from file if provided
|
||||||
|
if config_file:
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
file_config = json.load(f)
|
||||||
|
config = DEFAULT_CONFIG.copy()
|
||||||
|
config.update(file_config)
|
||||||
|
else:
|
||||||
|
config = DEFAULT_CONFIG.copy()
|
||||||
|
|
||||||
|
# Override config with command line arguments
|
||||||
|
config["max_debate_rounds"] = research_depth
|
||||||
|
config["max_risk_discuss_rounds"] = research_depth
|
||||||
|
config["quick_think_llm"] = shallow_thinker
|
||||||
|
config["deep_think_llm"] = deep_thinker
|
||||||
|
config["backend_url"] = backend_url
|
||||||
|
config["llm_provider"] = llm_provider.lower()
|
||||||
|
config["results_dir"] = output_dir
|
||||||
|
|
||||||
|
# Initialize the graph
|
||||||
|
graph = TradingAgentsGraph(analysts, config=config, debug=not quiet)
|
||||||
|
|
||||||
|
# Create result directory
|
||||||
|
results_dir = Path(output_dir) / ticker / analysis_date
|
||||||
|
results_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
report_dir = results_dir / "reports"
|
||||||
|
report_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
console.print(f"[green]Starting analysis for {ticker} on {analysis_date}[/green]")
|
||||||
|
console.print(f"[cyan]Analysts: {', '.join(analysts)}[/cyan]")
|
||||||
|
console.print(f"[cyan]Research depth: {research_depth} rounds[/cyan]")
|
||||||
|
console.print(f"[cyan]Output directory: {output_dir}[/cyan]")
|
||||||
|
|
||||||
|
# Initialize state and run analysis
|
||||||
|
init_agent_state = graph.propagator.create_initial_state(ticker, analysis_date)
|
||||||
|
args = graph.propagator.get_graph_args()
|
||||||
|
|
||||||
|
# Stream the analysis
|
||||||
|
final_state = None
|
||||||
|
if not quiet:
|
||||||
|
# Show progress with spinner
|
||||||
|
with console.status("[bold green]Running analysis...") as status:
|
||||||
|
trace = []
|
||||||
|
for chunk in graph.graph.stream(init_agent_state, **args):
|
||||||
|
trace.append(chunk)
|
||||||
|
if chunk.get("current_agent"):
|
||||||
|
status.update(f"[bold green]Running: {chunk['current_agent']}...")
|
||||||
|
final_state = trace[-1]
|
||||||
|
else:
|
||||||
|
# Run silently
|
||||||
|
trace = list(graph.graph.stream(init_agent_state, **args))
|
||||||
|
final_state = trace[-1]
|
||||||
|
|
||||||
|
# Process the final decision
|
||||||
|
decision = graph.process_signal(final_state["final_trade_decision"])
|
||||||
|
|
||||||
|
# Save reports
|
||||||
|
report_sections = {
|
||||||
|
"market_report": "Market Analysis",
|
||||||
|
"sentiment_report": "Social Sentiment",
|
||||||
|
"news_report": "News Analysis",
|
||||||
|
"fundamentals_report": "Fundamentals Analysis",
|
||||||
|
"investment_plan": "Research Team Decision",
|
||||||
|
"trader_investment_plan": "Trading Team Plan",
|
||||||
|
"final_trade_decision": "Portfolio Management Decision"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save individual reports
|
||||||
|
for section, title in report_sections.items():
|
||||||
|
if section in final_state and final_state[section]:
|
||||||
|
report_file = report_dir / f"{section}.md"
|
||||||
|
with open(report_file, 'w') as f:
|
||||||
|
f.write(f"# {title}\n\n{final_state[section]}")
|
||||||
|
|
||||||
|
# Save complete report
|
||||||
|
complete_report = []
|
||||||
|
complete_report.append(f"# Trading Analysis Report: {ticker}")
|
||||||
|
complete_report.append(f"**Date:** {analysis_date}")
|
||||||
|
complete_report.append(f"**Analysts:** {', '.join(analysts)}")
|
||||||
|
complete_report.append(f"**Research Depth:** {research_depth} rounds")
|
||||||
|
complete_report.append("")
|
||||||
|
|
||||||
|
for section, title in report_sections.items():
|
||||||
|
if section in final_state and final_state[section]:
|
||||||
|
complete_report.append(f"## {title}")
|
||||||
|
complete_report.append(final_state[section])
|
||||||
|
complete_report.append("")
|
||||||
|
|
||||||
|
complete_report_file = results_dir / "complete_report.md"
|
||||||
|
with open(complete_report_file, 'w') as f:
|
||||||
|
f.write("\n".join(complete_report))
|
||||||
|
|
||||||
|
# Save decision as JSON
|
||||||
|
decision_file = results_dir / "decision.json"
|
||||||
|
with open(decision_file, 'w') as f:
|
||||||
|
json.dump(decision, f, indent=2)
|
||||||
|
|
||||||
|
if not quiet:
|
||||||
|
console.print("[green]Analysis completed![/green]")
|
||||||
|
console.print(f"[cyan]Reports saved to: {results_dir}[/cyan]")
|
||||||
|
console.print(f"[cyan]Decision: {decision}[/cyan]")
|
||||||
|
else:
|
||||||
|
# In quiet mode, just output the decision
|
||||||
|
print(json.dumps(decision, indent=2))
|
||||||
|
|
||||||
|
return decision
|
||||||
|
|
||||||
|
|
||||||
|
def main(
|
||||||
|
ticker: str = typer.Option(None, "--ticker", "-t", help="Stock ticker symbol (e.g., AAPL, TSLA)"),
|
||||||
|
date: str = typer.Option(None, "--date", "-d", help="Analysis date (YYYY-MM-DD)"),
|
||||||
|
analysts: str = typer.Option(None, "--analysts", "-a", help="Comma-separated list of analysts (market,social,news,fundamentals)"),
|
||||||
|
depth: str = typer.Option(None, "--depth", help="Research depth (shallow=1, medium=3, deep=5)"),
|
||||||
|
llm_provider: str = typer.Option(None, "--llm-provider", help="LLM provider (openai, anthropic, google)"),
|
||||||
|
backend_url: str = typer.Option(None, "--backend-url", help="LLM backend URL"),
|
||||||
|
shallow_model: str = typer.Option(None, "--shallow-model", help="Model for shallow thinking"),
|
||||||
|
deep_model: str = typer.Option(None, "--deep-model", help="Model for deep thinking"),
|
||||||
|
output_dir: str = typer.Option("./results", "--output-dir", "-o", help="Output directory for reports"),
|
||||||
|
config_file: Optional[str] = typer.Option(None, "--config", "-c", help="Path to configuration file"),
|
||||||
|
headless: bool = typer.Option(False, "--headless", help="Run in headless mode without interactive prompts"),
|
||||||
|
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress verbose output (only show final results)"),
|
||||||
|
):
|
||||||
|
"""Run trading analysis with either interactive or headless mode."""
|
||||||
|
|
||||||
|
if headless:
|
||||||
|
# Headless mode - validate required arguments
|
||||||
|
if not ticker:
|
||||||
|
raise typer.BadParameter("--ticker is required in headless mode")
|
||||||
|
|
||||||
|
# Set defaults for optional arguments
|
||||||
|
if not date:
|
||||||
|
date = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||||
|
if not analysts:
|
||||||
|
analysts = "market,social,news,fundamentals"
|
||||||
|
if not depth:
|
||||||
|
depth = "medium"
|
||||||
|
if not llm_provider:
|
||||||
|
llm_provider = "openai"
|
||||||
|
if not backend_url:
|
||||||
|
backend_url = "https://api.openai.com/v1"
|
||||||
|
if not shallow_model:
|
||||||
|
shallow_model = "gpt-4o-mini"
|
||||||
|
if not deep_model:
|
||||||
|
deep_model = "o1-mini"
|
||||||
|
|
||||||
|
# Parse depth to number
|
||||||
|
depth_map = {"shallow": 1, "medium": 3, "deep": 5}
|
||||||
|
research_depth = depth_map.get(depth, 3)
|
||||||
|
|
||||||
|
# Parse analysts list
|
||||||
|
analyst_list = [a.strip() for a in analysts.split(",")]
|
||||||
|
|
||||||
|
# Validate analysts
|
||||||
|
valid_analysts = ["market", "social", "news", "fundamentals"]
|
||||||
|
for analyst in analyst_list:
|
||||||
|
if analyst not in valid_analysts:
|
||||||
|
raise typer.BadParameter(f"Invalid analyst: {analyst}. Valid options: {', '.join(valid_analysts)}")
|
||||||
|
|
||||||
|
# Run headless analysis
|
||||||
|
run_headless_analysis(
|
||||||
|
ticker=ticker,
|
||||||
|
analysis_date=date,
|
||||||
|
analysts=analyst_list,
|
||||||
|
research_depth=research_depth,
|
||||||
|
llm_provider=llm_provider,
|
||||||
|
backend_url=backend_url,
|
||||||
|
shallow_thinker=shallow_model,
|
||||||
|
deep_thinker=deep_model,
|
||||||
|
output_dir=output_dir,
|
||||||
|
quiet=quiet,
|
||||||
|
config_file=config_file,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Interactive mode (original behavior)
|
||||||
|
run_analysis()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app()
|
typer.run(main)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"llm_provider": "openai",
|
||||||
|
"deep_think_llm": "o1-mini",
|
||||||
|
"quick_think_llm": "gpt-4o-mini",
|
||||||
|
"backend_url": "https://api.openai.com/v1",
|
||||||
|
"max_debate_rounds": 3,
|
||||||
|
"max_risk_discuss_rounds": 3,
|
||||||
|
"online_tools": true,
|
||||||
|
"results_dir": "./results"
|
||||||
|
}
|
||||||
|
|
@ -22,5 +22,6 @@ redis
|
||||||
chainlit
|
chainlit
|
||||||
rich
|
rich
|
||||||
questionary
|
questionary
|
||||||
|
typer
|
||||||
langchain_anthropic
|
langchain_anthropic
|
||||||
langchain-google-genai
|
langchain-google-genai
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue