Upload folder using huggingface_hub
Browse files- README.md +15 -81
- pyproject.toml +64 -40
- run.py +102 -40
- src/stock-investment-analyst/Dockerfile +36 -0
- src/stock-investment-analyst/README.md +28 -0
- src/stock-investment-analyst/aagents/__init__.py +16 -0
- src/stock-investment-analyst/aagents/decision_agent.py +50 -0
- src/stock-investment-analyst/aagents/news_agent.py +38 -0
- src/stock-investment-analyst/aagents/sentiment_agent.py +32 -0
- src/stock-investment-analyst/aagents/stock_trends_agent.py +34 -0
- src/stock-investment-analyst/app.py +120 -0
- src/stock-investment-analyst/core/__init__.py +4 -0
- src/stock-investment-analyst/core/model.py +32 -0
- src/stock-investment-analyst/teams/__init__.py +3 -0
- src/stock-investment-analyst/teams/investment_team.py +49 -0
- src/stock-investment-analyst/tools/__init__.py +4 -0
- src/stock-investment-analyst/tools/search_tools.py +127 -0
- src/stock-investment-analyst/tools/yf_tools.py +277 -0
- src/stock-investment-analyst/utility/__init__.py +0 -0
- src/stock-investment-analyst/utility/autogen_model_factory.py +112 -0
- uv.lock +0 -0
README.md
CHANGED
|
@@ -1,94 +1,28 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
sdk_version: "0.0.1"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
short_description: Multi-
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
##
|
| 19 |
-
- **Team of Agents**: Collaborative analysis from Trends, News, Sentiment, and Decision agents.
|
| 20 |
-
- **Round-Robin Orchestration**: Agents take turns sharing insights in a structured conversation.
|
| 21 |
-
- **Real-time Data**: Fetches live stock history and financial data via `yfinance`.
|
| 22 |
-
- **News Integration**: Searches DuckDuckGo for the latest market news.
|
| 23 |
-
- **Streamlit UI**: Clean, interactive interface with agent avatars and real-time streaming.
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
|
| 30 |
-
##
|
| 31 |
-
- **Yahoo Finance**: Historical prices, analyst recommendations, market sentiment.
|
| 32 |
-
- **DuckDuckGo**: Live news search.
|
| 33 |
-
- **Web Scraping**: Fetching and summarizing full article content.
|
| 34 |
|
| 35 |
-
|
| 36 |
|
| 37 |
-
|
| 38 |
-
stock-analyst/
|
| 39 |
-
├── app.py # Main Streamlit UI
|
| 40 |
-
├── aagents/
|
| 41 |
-
│ ├── agents.py # Agent definitions (Trends, News, Sentiment, Decision) and factories
|
| 42 |
-
├── teams/
|
| 43 |
-
│ ├── team.py # RoundRobinGroupChat orchestration logic
|
| 44 |
-
├── tools/
|
| 45 |
-
│ ├── yf_tools.py # Yahoo Finance tool wrappers
|
| 46 |
-
│ ├── search_tools.py # DuckDuckGo and web scraping tools
|
| 47 |
-
├── Dockerfile # Deployment configuration
|
| 48 |
-
└── README.md # Project documentation
|
| 49 |
-
```
|
| 50 |
-
|
| 51 |
-
## Agents (`aagents/agents.py`)
|
| 52 |
-
|
| 53 |
-
- **Stock Trends Agent** (📈):
|
| 54 |
-
- Fetches historical price data.
|
| 55 |
-
- Analyzes price movements and volume trends.
|
| 56 |
-
- Outputs structured `StockTrend` data for UI visualization.
|
| 57 |
-
|
| 58 |
-
- **News Agent** (📰):
|
| 59 |
-
- Searches for top recent news stories.
|
| 60 |
-
- Fetches and reads full article content.
|
| 61 |
-
- Summarizes key events impacting the stock.
|
| 62 |
-
|
| 63 |
-
- **Sentiment Agent** (💡):
|
| 64 |
-
- Check general market sentiment.
|
| 65 |
-
- Reviews analyst recommendations.
|
| 66 |
-
- Aggregates expert opinions.
|
| 67 |
-
|
| 68 |
-
- **Decision Agent** (⚖️):
|
| 69 |
-
- Synthesizes all gathered information.
|
| 70 |
-
- Provides a final "Invest" or "Not Invest" decision.
|
| 71 |
-
- Summarizes the rationale.
|
| 72 |
-
|
| 73 |
-
## Key Technologies
|
| 74 |
-
|
| 75 |
-
| Component | Technology | Purpose |
|
| 76 |
-
|-----------|-----------|---------|
|
| 77 |
-
| Agent Framework | Microsoft AutoGen | Multi-agent orchestration |
|
| 78 |
-
| LLM | GPT-4o / Gemini | Intelligence engine for agents |
|
| 79 |
-
| UI Framework | Streamlit | User interface |
|
| 80 |
-
| Data Source | yfinance | Stock market data |
|
| 81 |
-
| Search | DuckDuckGo | Real-time news |
|
| 82 |
-
|
| 83 |
-
## Running Locally
|
| 84 |
-
|
| 85 |
-
```bash
|
| 86 |
-
# Install dependencies
|
| 87 |
-
uv sync
|
| 88 |
-
|
| 89 |
-
# Set environment variables in .env or shell
|
| 90 |
-
export GOOGLE_API_KEY="your-gemini-key" # or OPENAI_API_KEY if using OpenAI
|
| 91 |
-
|
| 92 |
-
# Run the Streamlit app (from the root)
|
| 93 |
-
streamlit run src/stock-analyst/app.py
|
| 94 |
-
```
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Midas
|
| 3 |
+
emoji: 🪙
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
app_file: app.py
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
short_description: Multi-agent investment team for stock analysis & decisions
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Midas
|
| 14 |
|
| 15 |
+
Midas — the king with the golden touch — is a multi-agent investment analysis team. Four specialist agents evaluate a stock from every angle and converge on a final investment recommendation.
|
| 16 |
|
| 17 |
+
## What it does
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
+
- **Trend Agent** — price history, moving averages, momentum signals
|
| 20 |
+
- **News Agent** — latest headlines and sentiment for the ticker
|
| 21 |
+
- **Sentiment Agent** — quantified market mood from news and social signals
|
| 22 |
+
- **Decision Agent** — synthesises all inputs into a Buy / Hold / Sell verdict
|
| 23 |
|
| 24 |
+
## Stack
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
Streamlit · OpenAI Agents SDK · Yahoo Finance · Google Gemini / GPT-4o
|
| 27 |
|
| 28 |
+
> **Disclaimer:** For informational purposes only. Not financial advice.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pyproject.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
[project]
|
| 2 |
-
name = "
|
| 3 |
version = "0.1.0"
|
| 4 |
description = "Agentic AI project"
|
| 5 |
readme = "README.md"
|
|
@@ -12,128 +12,156 @@ dependencies = [
|
|
| 12 |
"openai>=2.8.1",
|
| 13 |
"openai-agents>=0.5.1",
|
| 14 |
"anthropic>=0.49.0",
|
| 15 |
-
"langchain-openai>=1.
|
| 16 |
"langchain-anthropic>=1.1.0",
|
| 17 |
"langchain_huggingface>=1.1.0",
|
| 18 |
"langchain_ollama>=1.0.0",
|
| 19 |
"langchain_google_genai>=3.0.3",
|
| 20 |
"langchain_groq>=1.0.1",
|
| 21 |
-
|
| 22 |
-
|
| 23 |
# =======================
|
| 24 |
# LANGCHAIN / LANGGRAPH
|
| 25 |
# =======================
|
| 26 |
"langchain>=1.0.7",
|
| 27 |
"langchain-community>=0.4.1",
|
| 28 |
-
"langgraph>=1.0.
|
| 29 |
"langgraph-checkpoint-sqlite>=3.0.0",
|
| 30 |
-
"langsmith>=0.
|
| 31 |
-
"langchain-text-splitters>=1.
|
| 32 |
"langchain-chroma>=1.0.0",
|
| 33 |
"html2text>=2025.4.15",
|
| 34 |
"traceloop-sdk>=0.33.0",
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
# =======================
|
| 37 |
# VECTOR DB / INDEXING
|
| 38 |
# =======================
|
| 39 |
"faiss-cpu>=1.13.0",
|
| 40 |
-
"chromadb=
|
| 41 |
"sentence-transformers>=5.1.2",
|
| 42 |
"pymupdf",
|
| 43 |
-
"pypdf>=6.
|
| 44 |
"pypdf2>=3.0.1",
|
| 45 |
"arxiv>=2.3.1",
|
| 46 |
"wikipedia>=1.4.0",
|
| 47 |
-
|
| 48 |
# =======================
|
| 49 |
# AUTOGEN
|
| 50 |
# =======================
|
| 51 |
-
"autogen-agentchat
|
| 52 |
-
"autogen-ext[grpc,mcp,ollama,openai]
|
| 53 |
"asyncio",
|
| 54 |
-
|
| 55 |
# =======================
|
| 56 |
# MCP
|
| 57 |
# =======================
|
| 58 |
"mcp-server-fetch>=2025.1.17",
|
| 59 |
"mcp[cli]>=1.21.2",
|
| 60 |
-
|
| 61 |
# =======================
|
| 62 |
# NETWORKING / UTILITIES
|
| 63 |
# =======================
|
| 64 |
"psutil>=7.0.0",
|
| 65 |
-
"python-dotenv>=1.
|
| 66 |
-
"requests>=2.
|
| 67 |
-
"aiohttp>=3.
|
| 68 |
"httpx>=0.28.1",
|
| 69 |
"speedtest-cli>=2.1.3",
|
| 70 |
"logfire",
|
| 71 |
"google-search-results",
|
| 72 |
"smithery>=0.4.4",
|
| 73 |
"sendgrid",
|
| 74 |
-
|
| 75 |
# =======================
|
| 76 |
# WEB SCRAPING
|
| 77 |
# =======================
|
| 78 |
"playwright>=1.51.0",
|
| 79 |
"beautifulsoup4>=4.12.3",
|
| 80 |
-
"lxml>=
|
| 81 |
-
|
| 82 |
# =======================
|
| 83 |
# FINANCE / NLP
|
| 84 |
# =======================
|
| 85 |
"yfinance>=0.2.66",
|
| 86 |
"textblob>=0.17.1",
|
| 87 |
"polygon-api-client>=1.16.3",
|
| 88 |
-
|
| 89 |
# =======================
|
| 90 |
# VISUAL / UI / PDF
|
| 91 |
# =======================
|
| 92 |
"plotly>=6.5.0",
|
| 93 |
-
"streamlit>=1.
|
| 94 |
"reportlab>=4.4.5",
|
| 95 |
"fastapi",
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
# =======================
|
| 98 |
# AUDIO / VIDEO
|
| 99 |
# =======================
|
| 100 |
-
"yt_dlp>=
|
| 101 |
-
"openai-whisper
|
| 102 |
-
|
|
|
|
| 103 |
# =======================
|
| 104 |
# MACHINE LEARNING
|
| 105 |
# =======================
|
| 106 |
"scikit-learn>=1.7.2",
|
| 107 |
-
"huggingface_hub
|
| 108 |
"datasets>=4.4.1",
|
| 109 |
-
|
| 110 |
# =======================
|
| 111 |
# IPYNB SUPPORT
|
| 112 |
# =======================
|
| 113 |
"ipykernel>=7.1.0",
|
| 114 |
-
|
| 115 |
# =======================
|
| 116 |
# TOOLS
|
| 117 |
# =======================
|
| 118 |
"ddgs>=9.9.2",
|
| 119 |
"duckduckgo_search",
|
| 120 |
"azure-identity>=1.25.1",
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
# =======================
|
| 123 |
# OBSERVABILITY
|
| 124 |
# =======================
|
|
|
|
| 125 |
"openinference-instrumentation-autogen>=0.1.0",
|
| 126 |
"openinference-instrumentation-openai>=0.1.15",
|
| 127 |
"opentelemetry-sdk>=1.20.0",
|
| 128 |
"opentelemetry-exporter-otlp>=1.20.0",
|
| 129 |
"opentelemetry-api>=1.20.0",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
]
|
| 131 |
|
| 132 |
[dependency-groups]
|
| 133 |
dev = [
|
| 134 |
-
"pytest>=
|
| 135 |
"ipykernel>=7.1.0",
|
| 136 |
"pytest-asyncio",
|
|
|
|
| 137 |
]
|
| 138 |
|
| 139 |
# ============================================================
|
|
@@ -149,20 +177,16 @@ build-backend = "setuptools.build_meta"
|
|
| 149 |
# PACKAGING & DISCOVERY
|
| 150 |
# ============================================================
|
| 151 |
# Tells setuptools where to find the source code.
|
| 152 |
-
# This makes 'common' and 'src' importable when installed (pip install -e .).
|
| 153 |
[tool.setuptools.packages.find]
|
| 154 |
-
where = ["."]
|
| 155 |
-
include = ["
|
| 156 |
|
| 157 |
|
| 158 |
# ============================================================
|
| 159 |
# PYTEST SETTINGS
|
| 160 |
# ============================================================
|
| 161 |
-
# Configures the test runner to automatically find code.
|
| 162 |
[tool.pytest.ini_options]
|
| 163 |
-
|
| 164 |
-
# This allows tests to import modules (e.g., 'import travel_agent')
|
| 165 |
-
# just like the apps do locally, preventing ModuleNotFoundError.
|
| 166 |
-
pythonpath = ["src", "common"]
|
| 167 |
testpaths = ["tests"] # Only look for tests in the 'tests' directory
|
| 168 |
addopts = "-q" # Run in quiet mode (less verbose output)
|
|
|
|
|
|
| 1 |
[project]
|
| 2 |
+
name = "agenticai"
|
| 3 |
version = "0.1.0"
|
| 4 |
description = "Agentic AI project"
|
| 5 |
readme = "README.md"
|
|
|
|
| 12 |
"openai>=2.8.1",
|
| 13 |
"openai-agents>=0.5.1",
|
| 14 |
"anthropic>=0.49.0",
|
| 15 |
+
"langchain-openai>=1.1.14",
|
| 16 |
"langchain-anthropic>=1.1.0",
|
| 17 |
"langchain_huggingface>=1.1.0",
|
| 18 |
"langchain_ollama>=1.0.0",
|
| 19 |
"langchain_google_genai>=3.0.3",
|
| 20 |
"langchain_groq>=1.0.1",
|
|
|
|
|
|
|
| 21 |
# =======================
|
| 22 |
# LANGCHAIN / LANGGRAPH
|
| 23 |
# =======================
|
| 24 |
"langchain>=1.0.7",
|
| 25 |
"langchain-community>=0.4.1",
|
| 26 |
+
"langgraph>=1.0.10rc1",
|
| 27 |
"langgraph-checkpoint-sqlite>=3.0.0",
|
| 28 |
+
"langsmith>=0.8.0",
|
| 29 |
+
"langchain-text-splitters>=1.1.2",
|
| 30 |
"langchain-chroma>=1.0.0",
|
| 31 |
"html2text>=2025.4.15",
|
| 32 |
"traceloop-sdk>=0.33.0",
|
| 33 |
+
# ==================================
|
| 34 |
+
# LlamIndex
|
| 35 |
+
# ==================================
|
| 36 |
+
"llama-index>=0.14.15",
|
| 37 |
+
"llama-index-llms-google-genai>=0.8.7",
|
| 38 |
+
"llama-index-embeddings-google-genai>=0.3.2",
|
| 39 |
+
"google-generativeai>=0.8.6",
|
| 40 |
+
# =======================
|
| 41 |
+
# MICROSOFT AGENT FRAMEWORK
|
| 42 |
+
# =======================
|
| 43 |
+
#"agent-framework==1.0.0b251204",
|
| 44 |
+
#"agent-framework-azure-ai==1.0.0b251204",
|
| 45 |
+
#"azure-ai-projects",
|
| 46 |
+
#"azure-ai-agents",
|
| 47 |
+
#"azure-ai-agents>=1.2.0b5",
|
| 48 |
+
#"agent-framework-azure-ai",
|
| 49 |
# =======================
|
| 50 |
# VECTOR DB / INDEXING
|
| 51 |
# =======================
|
| 52 |
"faiss-cpu>=1.13.0",
|
| 53 |
+
"chromadb>=0.4.0",
|
| 54 |
"sentence-transformers>=5.1.2",
|
| 55 |
"pymupdf",
|
| 56 |
+
"pypdf>=6.10.2",
|
| 57 |
"pypdf2>=3.0.1",
|
| 58 |
"arxiv>=2.3.1",
|
| 59 |
"wikipedia>=1.4.0",
|
|
|
|
| 60 |
# =======================
|
| 61 |
# AUTOGEN
|
| 62 |
# =======================
|
| 63 |
+
"autogen-agentchat==0.7.5",
|
| 64 |
+
"autogen-ext[grpc,mcp,ollama,openai]==0.7.5",
|
| 65 |
"asyncio",
|
| 66 |
+
"phidata>=2.0.0",
|
| 67 |
# =======================
|
| 68 |
# MCP
|
| 69 |
# =======================
|
| 70 |
"mcp-server-fetch>=2025.1.17",
|
| 71 |
"mcp[cli]>=1.21.2",
|
|
|
|
| 72 |
# =======================
|
| 73 |
# NETWORKING / UTILITIES
|
| 74 |
# =======================
|
| 75 |
"psutil>=7.0.0",
|
| 76 |
+
"python-dotenv>=1.2.2",
|
| 77 |
+
"requests>=2.33.0",
|
| 78 |
+
"aiohttp>=3.14.0",
|
| 79 |
"httpx>=0.28.1",
|
| 80 |
"speedtest-cli>=2.1.3",
|
| 81 |
"logfire",
|
| 82 |
"google-search-results",
|
| 83 |
"smithery>=0.4.4",
|
| 84 |
"sendgrid",
|
|
|
|
| 85 |
# =======================
|
| 86 |
# WEB SCRAPING
|
| 87 |
# =======================
|
| 88 |
"playwright>=1.51.0",
|
| 89 |
"beautifulsoup4>=4.12.3",
|
| 90 |
+
"lxml>=6.1.0",
|
|
|
|
| 91 |
# =======================
|
| 92 |
# FINANCE / NLP
|
| 93 |
# =======================
|
| 94 |
"yfinance>=0.2.66",
|
| 95 |
"textblob>=0.17.1",
|
| 96 |
"polygon-api-client>=1.16.3",
|
|
|
|
| 97 |
# =======================
|
| 98 |
# VISUAL / UI / PDF
|
| 99 |
# =======================
|
| 100 |
"plotly>=6.5.0",
|
| 101 |
+
"streamlit>=1.54.0",
|
| 102 |
"reportlab>=4.4.5",
|
| 103 |
"fastapi",
|
| 104 |
+
"Pillow",
|
| 105 |
+
"python-docx",
|
| 106 |
+
"matplotlib",
|
| 107 |
+
"fpdf",
|
| 108 |
+
"extra-streamlit-components",
|
| 109 |
+
"nest_asyncio",
|
| 110 |
# =======================
|
| 111 |
# AUDIO / VIDEO
|
| 112 |
# =======================
|
| 113 |
+
"yt_dlp>=2026.2.21",
|
| 114 |
+
"openai-whisper==20250625",
|
| 115 |
+
"numba==0.63.1",
|
| 116 |
+
"llvmlite==0.46.0",
|
| 117 |
# =======================
|
| 118 |
# MACHINE LEARNING
|
| 119 |
# =======================
|
| 120 |
"scikit-learn>=1.7.2",
|
| 121 |
+
"huggingface_hub>=0.23.2",
|
| 122 |
"datasets>=4.4.1",
|
|
|
|
| 123 |
# =======================
|
| 124 |
# IPYNB SUPPORT
|
| 125 |
# =======================
|
| 126 |
"ipykernel>=7.1.0",
|
|
|
|
| 127 |
# =======================
|
| 128 |
# TOOLS
|
| 129 |
# =======================
|
| 130 |
"ddgs>=9.9.2",
|
| 131 |
"duckduckgo_search",
|
| 132 |
"azure-identity>=1.25.1",
|
| 133 |
+
"azure-mgmt-resource>=23.0.1",
|
| 134 |
+
"azure-mgmt-compute>=30.3.0",
|
| 135 |
+
"azure-mgmt-monitor>=6.0.2",
|
| 136 |
+
"azure-monitor-query>=1.2.0",
|
| 137 |
+
"PyGithub>=2.1.1",
|
| 138 |
# =======================
|
| 139 |
# OBSERVABILITY
|
| 140 |
# =======================
|
| 141 |
+
"langfuse>=3.0.0",
|
| 142 |
"openinference-instrumentation-autogen>=0.1.0",
|
| 143 |
"openinference-instrumentation-openai>=0.1.15",
|
| 144 |
"opentelemetry-sdk>=1.20.0",
|
| 145 |
"opentelemetry-exporter-otlp>=1.20.0",
|
| 146 |
"opentelemetry-api>=1.20.0",
|
| 147 |
+
# =======================
|
| 148 |
+
# Google Authentication
|
| 149 |
+
# =======================
|
| 150 |
+
"google-auth>=2.22.0",
|
| 151 |
+
"google-auth-oauthlib>=0.4.6",
|
| 152 |
+
"google-auth-httplib2>=0.1.0",
|
| 153 |
+
"autoflake>=1.5.0",
|
| 154 |
+
"psycopg2-binary>=2.9.9",
|
| 155 |
+
"sqlalchemy>=2.0.46",
|
| 156 |
+
"llama-index-readers-database>=0.5.1",
|
| 157 |
]
|
| 158 |
|
| 159 |
[dependency-groups]
|
| 160 |
dev = [
|
| 161 |
+
"pytest>=9.0.3",
|
| 162 |
"ipykernel>=7.1.0",
|
| 163 |
"pytest-asyncio",
|
| 164 |
+
"pytest-cov>=7.1.0",
|
| 165 |
]
|
| 166 |
|
| 167 |
# ============================================================
|
|
|
|
| 177 |
# PACKAGING & DISCOVERY
|
| 178 |
# ============================================================
|
| 179 |
# Tells setuptools where to find the source code.
|
|
|
|
| 180 |
[tool.setuptools.packages.find]
|
| 181 |
+
where = ["."]
|
| 182 |
+
include = ["src*"]
|
| 183 |
|
| 184 |
|
| 185 |
# ============================================================
|
| 186 |
# PYTEST SETTINGS
|
| 187 |
# ============================================================
|
|
|
|
| 188 |
[tool.pytest.ini_options]
|
| 189 |
+
pythonpath = ["src"]
|
|
|
|
|
|
|
|
|
|
| 190 |
testpaths = ["tests"] # Only look for tests in the 'tests' directory
|
| 191 |
addopts = "-q" # Run in quiet mode (less verbose output)
|
| 192 |
+
asyncio_mode = "auto" # Auto-detect async tests (requires pytest-asyncio)
|
run.py
CHANGED
|
@@ -16,9 +16,10 @@ import sys
|
|
| 16 |
import os
|
| 17 |
import subprocess
|
| 18 |
import argparse
|
|
|
|
| 19 |
from pathlib import Path
|
| 20 |
from typing import Dict, Optional
|
| 21 |
-
from agents import Runner, SQLiteSession
|
| 22 |
# from agents import set_trace_processors
|
| 23 |
# from langsmith.wrappers import OpenAIAgentsTracingProcessor
|
| 24 |
|
|
@@ -28,46 +29,49 @@ load_dotenv(override=True)
|
|
| 28 |
|
| 29 |
# App registry - maps app names to their paths and entry points
|
| 30 |
APP_REGISTRY: Dict[str, Dict[str, str]] = {
|
| 31 |
-
"
|
| 32 |
-
"path": "src/
|
| 33 |
"entry": "app.py",
|
| 34 |
-
"description": "
|
| 35 |
},
|
| 36 |
"deep-research": {
|
| 37 |
-
"path": "src/deep-research",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
"entry": "app.py",
|
| 39 |
-
"description": "
|
| 40 |
},
|
| 41 |
"stock-analyst": {
|
| 42 |
-
"path": "src/stock-analyst",
|
| 43 |
"entry": "app.py",
|
| 44 |
-
"description": "Stock Analyst -
|
| 45 |
},
|
| 46 |
-
"travel-
|
| 47 |
-
"path": "src/travel-
|
| 48 |
"entry": "app.py",
|
| 49 |
-
"description": "Travel
|
| 50 |
},
|
| 51 |
-
"trip-planner": {
|
| 52 |
-
"path": "src/trip-planner",
|
| 53 |
"entry": "main.py",
|
| 54 |
-
"
|
|
|
|
| 55 |
},
|
| 56 |
-
"
|
| 57 |
-
"path": "src/
|
| 58 |
-
"entry": "
|
| 59 |
-
"
|
|
|
|
| 60 |
},
|
| 61 |
-
"
|
| 62 |
-
"path": "
|
| 63 |
-
"entry": "
|
| 64 |
-
"
|
|
|
|
| 65 |
},
|
| 66 |
-
"literature-review": {
|
| 67 |
-
"path": "src/literature-review",
|
| 68 |
-
"entry": "app.py",
|
| 69 |
-
"description": "Literature Review Assistant - Multi-agent literature review tool"
|
| 70 |
-
}
|
| 71 |
}
|
| 72 |
|
| 73 |
|
|
@@ -142,33 +146,91 @@ def launch_app(app_name: str, port: Optional[int] = None):
|
|
| 142 |
print(f"📂 Location: {config['path']}")
|
| 143 |
print(f"🌐 Entry Point: {app_file}")
|
| 144 |
|
| 145 |
-
|
| 146 |
-
cmd = ["streamlit", "run", app_file]
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
else:
|
| 153 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
print("\n" + "=" * 70)
|
| 156 |
print("\n🎯 Starting application...\n")
|
| 157 |
|
| 158 |
-
# Prepare environment with project root in PYTHONPATH to fix imports
|
| 159 |
-
env = os.environ.copy()
|
| 160 |
-
env["PYTHONPATH"] = str(project_root) + os.pathsep + env.get("PYTHONPATH", "")
|
| 161 |
print(f"\n\nPYTHONPATH: {env['PYTHONPATH']}")
|
| 162 |
|
| 163 |
try:
|
| 164 |
# Change to app directory and run
|
| 165 |
os.chdir(app_dir)
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 167 |
except KeyboardInterrupt:
|
| 168 |
print("\n\n👋 Application stopped by user")
|
| 169 |
except FileNotFoundError:
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
sys.exit(1)
|
| 173 |
except Exception as e:
|
| 174 |
print(f"\n❌ Error launching app: {e}")
|
|
|
|
| 16 |
import os
|
| 17 |
import subprocess
|
| 18 |
import argparse
|
| 19 |
+
import time
|
| 20 |
from pathlib import Path
|
| 21 |
from typing import Dict, Optional
|
| 22 |
+
# from agents import Runner, SQLiteSession
|
| 23 |
# from agents import set_trace_processors
|
| 24 |
# from langsmith.wrappers import OpenAIAgentsTracingProcessor
|
| 25 |
|
|
|
|
| 29 |
|
| 30 |
# App registry - maps app names to their paths and entry points
|
| 31 |
APP_REGISTRY: Dict[str, Dict[str, str]] = {
|
| 32 |
+
"research-assistant": {
|
| 33 |
+
"path": "src/ai-research-assistant",
|
| 34 |
"entry": "app.py",
|
| 35 |
+
"description": "AI Research Assistant - Multi-specialist orchestrator for finance, news, and web research"
|
| 36 |
},
|
| 37 |
"deep-research": {
|
| 38 |
+
"path": "src/deep-research-reporter",
|
| 39 |
+
"entry": "app.py",
|
| 40 |
+
"description": "Deep Research Reporter - Plans, searches, and synthesises comprehensive research reports"
|
| 41 |
+
},
|
| 42 |
+
"healthcare": {
|
| 43 |
+
"path": "src/healthcare-rag-advisor",
|
| 44 |
"entry": "app.py",
|
| 45 |
+
"description": "Healthcare RAG Advisor - Medical information retrieval using RAG and web search"
|
| 46 |
},
|
| 47 |
"stock-analyst": {
|
| 48 |
+
"path": "src/stock-investment-analyst",
|
| 49 |
"entry": "app.py",
|
| 50 |
+
"description": "Stock Investment Analyst - Multi-agent investment team for technical and sentiment analysis"
|
| 51 |
},
|
| 52 |
+
"travel-planner": {
|
| 53 |
+
"path": "src/travel-planner",
|
| 54 |
"entry": "app.py",
|
| 55 |
+
"description": "Travel Planner - AI-powered trip planning with flight, hotel, and itinerary recommendations"
|
| 56 |
},
|
| 57 |
+
"trip-planner-api": {
|
| 58 |
+
"path": "src/trip-planner-api",
|
| 59 |
"entry": "main.py",
|
| 60 |
+
"type": "fastapi",
|
| 61 |
+
"description": "Trip Planner API - Phidata-powered trip itinerary planning REST API"
|
| 62 |
},
|
| 63 |
+
"market-analyst": {
|
| 64 |
+
"path": "src/market-analyst",
|
| 65 |
+
"entry": "backend/main.py",
|
| 66 |
+
"type": "fastapi",
|
| 67 |
+
"description": "AI Market Analyst - Real-time multi-agent market analysis with streaming (Vue.js + FastAPI)"
|
| 68 |
},
|
| 69 |
+
"test": {
|
| 70 |
+
"path": ".",
|
| 71 |
+
"entry": "tests",
|
| 72 |
+
"type": "test",
|
| 73 |
+
"description": "Run Project Tests - Executes pytest suite"
|
| 74 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
|
|
|
|
| 146 |
print(f"📂 Location: {config['path']}")
|
| 147 |
print(f"🌐 Entry Point: {app_file}")
|
| 148 |
|
| 149 |
+
app_type = config.get("type", "streamlit")
|
|
|
|
| 150 |
|
| 151 |
+
python_exe = sys.executable
|
| 152 |
+
is_windows = sys.platform == "win32"
|
| 153 |
+
|
| 154 |
+
# Prepare environment with project root in PYTHONPATH to fix imports
|
| 155 |
+
env = os.environ.copy()
|
| 156 |
+
env["PYTHONPATH"] = str(project_root) + os.pathsep + env.get("PYTHONPATH", "")
|
| 157 |
+
|
| 158 |
+
# Decoupled App Logic: Build frontend if needed
|
| 159 |
+
if app_name == "market-analyst":
|
| 160 |
+
frontend_dir = project_root / "src/market-analyst/frontend"
|
| 161 |
+
dist_dir = frontend_dir / "dist"
|
| 162 |
+
if not dist_dir.exists():
|
| 163 |
+
print("\n🛠️ Frontend build missing. Building now...")
|
| 164 |
+
subprocess.run(["npm", "run", "build"], cwd=frontend_dir, shell=is_windows)
|
| 165 |
+
print("✅ Frontend built.\n")
|
| 166 |
+
|
| 167 |
+
# App Type specific logic
|
| 168 |
+
if app_type == "fastapi":
|
| 169 |
+
# Extract module name from entry point (e.g. backend/main.py -> backend.main)
|
| 170 |
+
module_path = app_file.replace(".py", "").replace("/", ".").replace("\\", ".")
|
| 171 |
+
cmd = [python_exe, "-m", "uvicorn", f"{module_path}:app", "--host", "0.0.0.0"]
|
| 172 |
+
default_port = 8000
|
| 173 |
+
elif app_type == "script":
|
| 174 |
+
cmd = [python_exe, app_file]
|
| 175 |
+
default_port = None
|
| 176 |
+
elif app_type == "test":
|
| 177 |
+
cmd = [python_exe, "-m", "pytest", app_file, "-v"]
|
| 178 |
+
default_port = None
|
| 179 |
+
elif app_type == "npm":
|
| 180 |
+
cmd = ["npm", "run", "dev"]
|
| 181 |
+
default_port = 5173
|
| 182 |
else:
|
| 183 |
+
cmd = [python_exe, "-m", "streamlit", "run", app_file]
|
| 184 |
+
default_port = 8501
|
| 185 |
+
|
| 186 |
+
# Add port if specified
|
| 187 |
+
actual_port = port if port else default_port
|
| 188 |
+
|
| 189 |
+
if app_type in ["streamlit", "fastapi", "npm"]:
|
| 190 |
+
if port:
|
| 191 |
+
if app_type == "fastapi":
|
| 192 |
+
cmd.extend(["--port", str(port)])
|
| 193 |
+
elif app_type == "npm":
|
| 194 |
+
cmd.extend(["--", "--port", str(port)])
|
| 195 |
+
else:
|
| 196 |
+
cmd.extend(["--server.port", str(port)])
|
| 197 |
+
print(f"🔌 Port: {port}")
|
| 198 |
+
else:
|
| 199 |
+
print(f"🔌 Port: {default_port} (default)")
|
| 200 |
+
|
| 201 |
+
# Kill any process using the target port (Port is only relevant for web apps)
|
| 202 |
+
try:
|
| 203 |
+
import platform
|
| 204 |
+
if platform.system() != "Windows":
|
| 205 |
+
# Use fuser on Linux/Mac to kill processes on the port
|
| 206 |
+
kill_cmd = ["fuser", "-k", f"{actual_port}/tcp"]
|
| 207 |
+
subprocess.run(kill_cmd, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
| 208 |
+
print(f"🧹 Cleaned up port {actual_port}")
|
| 209 |
+
except Exception:
|
| 210 |
+
pass # Silently continue if cleanup fails
|
| 211 |
|
| 212 |
print("\n" + "=" * 70)
|
| 213 |
print("\n🎯 Starting application...\n")
|
| 214 |
|
|
|
|
|
|
|
|
|
|
| 215 |
print(f"\n\nPYTHONPATH: {env['PYTHONPATH']}")
|
| 216 |
|
| 217 |
try:
|
| 218 |
# Change to app directory and run
|
| 219 |
os.chdir(app_dir)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
subprocess.run(cmd, env=env, shell=is_windows)
|
| 224 |
except KeyboardInterrupt:
|
| 225 |
print("\n\n👋 Application stopped by user")
|
| 226 |
except FileNotFoundError:
|
| 227 |
+
binary = "command"
|
| 228 |
+
if app_type == "fastapi": binary = "uvicorn"
|
| 229 |
+
elif app_type == "streamlit": binary = "streamlit"
|
| 230 |
+
elif app_type == "test": binary = "pytest"
|
| 231 |
+
|
| 232 |
+
print(f"\n❌ Error: {binary} not found in the current environment.")
|
| 233 |
+
print(f" Please install it: pip install {binary}")
|
| 234 |
sys.exit(1)
|
| 235 |
except Exception as e:
|
| 236 |
print(f"\n❌ Error launching app: {e}")
|
src/stock-investment-analyst/Dockerfile
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 4 |
+
DEBIAN_FRONTEND=noninteractive \
|
| 5 |
+
PYTHONPATH=/app/src/stock-analyst:$PYTHONPATH
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
# System deps
|
| 10 |
+
RUN apt-get update && apt-get install -y \
|
| 11 |
+
git build-essential curl \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
# Install uv
|
| 15 |
+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
| 16 |
+
ENV PATH="/root/.local/bin:$PATH"
|
| 17 |
+
|
| 18 |
+
# Copy project metadata
|
| 19 |
+
COPY pyproject.toml .
|
| 20 |
+
COPY uv.lock .
|
| 21 |
+
|
| 22 |
+
# Copy application code
|
| 23 |
+
COPY common/ ./common/
|
| 24 |
+
COPY src/stock-analyst/ ./src/stock-analyst/
|
| 25 |
+
|
| 26 |
+
# Install dependencies using uv, then export and install with pip to system
|
| 27 |
+
# We use --no-dev to exclude dev dependencies if any
|
| 28 |
+
RUN uv sync --frozen --no-dev && \
|
| 29 |
+
uv pip install -e . --system
|
| 30 |
+
|
| 31 |
+
# Copy entry point
|
| 32 |
+
COPY run.py .
|
| 33 |
+
|
| 34 |
+
EXPOSE 7860
|
| 35 |
+
|
| 36 |
+
CMD ["python", "run.py", "stock-analyst", "--port", "7860"]
|
src/stock-investment-analyst/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Midas
|
| 3 |
+
emoji: 🪙
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: yellow
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: Multi-agent investment team for stock analysis & decisions
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Midas
|
| 14 |
+
|
| 15 |
+
Midas — the king with the golden touch — is a multi-agent investment analysis team. Four specialist agents evaluate a stock from every angle and converge on a final investment recommendation.
|
| 16 |
+
|
| 17 |
+
## What it does
|
| 18 |
+
|
| 19 |
+
- **Trend Agent** — price history, moving averages, momentum signals
|
| 20 |
+
- **News Agent** — latest headlines and sentiment for the ticker
|
| 21 |
+
- **Sentiment Agent** — quantified market mood from news and social signals
|
| 22 |
+
- **Decision Agent** — synthesises all inputs into a Buy / Hold / Sell verdict
|
| 23 |
+
|
| 24 |
+
## Stack
|
| 25 |
+
|
| 26 |
+
Streamlit · OpenAI Agents SDK · Yahoo Finance · Google Gemini / GPT-4o
|
| 27 |
+
|
| 28 |
+
> **Disclaimer:** For informational purposes only. Not financial advice.
|
src/stock-investment-analyst/aagents/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .stock_trends_agent import get_stock_trends_agent
|
| 2 |
+
from .news_agent import get_news_agent
|
| 3 |
+
from .sentiment_agent import get_sentiment_agent
|
| 4 |
+
from .decision_agent import get_decision_agent
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
class StockTrend(BaseModel):
|
| 8 |
+
stock_name: str
|
| 9 |
+
trade_date: str
|
| 10 |
+
open_price: float
|
| 11 |
+
close_price: float
|
| 12 |
+
high_price: float
|
| 13 |
+
low_price: float
|
| 14 |
+
volume: int
|
| 15 |
+
|
| 16 |
+
__all__ = ["get_stock_trends_agent", "get_news_agent", "get_sentiment_agent", "get_decision_agent", "StockTrend"]
|
src/stock-investment-analyst/aagents/decision_agent.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autogen_agentchat.agents import AssistantAgent
|
| 2 |
+
from core.model import get_model_client
|
| 3 |
+
|
| 4 |
+
def get_decision_agent():
|
| 5 |
+
# Upgrade to Pro model for better reasoning
|
| 6 |
+
# Using 1.5-pro as requested for better decision making
|
| 7 |
+
model_client = get_model_client()
|
| 8 |
+
|
| 9 |
+
decision_agent = AssistantAgent(
|
| 10 |
+
name="decision_agent",
|
| 11 |
+
model_client=model_client,
|
| 12 |
+
tools=[], # Decision agent usually synthesizes info, might not need tools if it consumes chat history
|
| 13 |
+
system_message=(
|
| 14 |
+
"You are the Decision Agent. Your role is to synthesize data from the Stock Trends, News, and Sentiment agents "
|
| 15 |
+
"to provide a final, well-reasoned investment recommendation.\n\n"
|
| 16 |
+
|
| 17 |
+
"**STEP 1: CALCULATE WEIGHTED SCORE**\n"
|
| 18 |
+
"You MUST score the stock on the following criteria (0-10 scale) and calculate the weighted total:\n"
|
| 19 |
+
"1. **Technical Indicators (Weight: 40%)**: Score 0-10 based on trend direction, moving averages, and volume.\n"
|
| 20 |
+
"2. **News Sentiment (Weight: 30%)**: Score 0-10 based on recent headlines and PR tone.\n"
|
| 21 |
+
"3. **Analyst Ratings (Weight: 30%)**: Score 0-10 based on analyst consensus and price targets.\n\n"
|
| 22 |
+
|
| 23 |
+
"**Formula**: `(Technical * 0.4) + (News * 0.3) + (Analyst * 0.3) = Total Score`\n\n"
|
| 24 |
+
|
| 25 |
+
"**STEP 2: DETERMINE DECISION**\n"
|
| 26 |
+
"- If **Total Score > 7.5** -> Decision: **INVEST**\n"
|
| 27 |
+
"- If **Total Score < 5.0** -> Decision: **AVOID**\n"
|
| 28 |
+
"- Else -> Decision: **WAIT**\n\n"
|
| 29 |
+
|
| 30 |
+
"**Output Requirement:**\n"
|
| 31 |
+
"You MUST provide your response in the following structured format:\n"
|
| 32 |
+
"1. **Decision**: [Invest / Wait / Avoid] (Based strictly on the rule above)\n"
|
| 33 |
+
"2. **Scoring Table**:\n"
|
| 34 |
+
" | Category | Score (0-10) | Weighted Score |\n"
|
| 35 |
+
" | :--- | :--- | :--- |\n"
|
| 36 |
+
" | Technicals (40%) | [Score] | [Val] |\n"
|
| 37 |
+
" | News (30%) | [Score] | [Val] |\n"
|
| 38 |
+
" | Analysts (30%) | [Score] | [Val] |\n"
|
| 39 |
+
" | **TOTAL** | | **[Total Score]** |\n"
|
| 40 |
+
"3. **Risk Level**: [Low / Medium / High]\n"
|
| 41 |
+
"4. **Reasoning**:\n"
|
| 42 |
+
" - **Pros**: [List top 3 positive factors]\n"
|
| 43 |
+
" - **Cons**: [List top 3 negative factors]\n"
|
| 44 |
+
"5. **Validation**: Briefly explain why the confidence score was chosen based on the consistency of the data.\n\n"
|
| 45 |
+
|
| 46 |
+
"Also provide the current stock price if available in the context.\n"
|
| 47 |
+
"End your response with 'Decision Made' once you finalize the decision."
|
| 48 |
+
)
|
| 49 |
+
)
|
| 50 |
+
return decision_agent
|
src/stock-investment-analyst/aagents/news_agent.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autogen_agentchat.agents import AssistantAgent
|
| 2 |
+
from autogen_core.tools import FunctionTool
|
| 3 |
+
from tools.search_tools import _duckduckgo_search, searchQuery
|
| 4 |
+
from core.model import get_model_client
|
| 5 |
+
|
| 6 |
+
def get_news_agent():
|
| 7 |
+
model_client = get_model_client()
|
| 8 |
+
|
| 9 |
+
async def news_search(query: str) -> str:
|
| 10 |
+
"""
|
| 11 |
+
Search for latest news regarding a topic or stock.
|
| 12 |
+
"""
|
| 13 |
+
# Anchor search to reputable sources as per Suggestion 3
|
| 14 |
+
# reputable_sources = " (site:bloomberg.com OR site:reuters.com OR site:cnbc.com OR site:wsj.com OR site:finance.yahoo.com)"
|
| 15 |
+
# if "site:" not in query:
|
| 16 |
+
# query += reputable_sources
|
| 17 |
+
|
| 18 |
+
# Use underlying function with proper params
|
| 19 |
+
params = searchQuery(query=query, search_type="news", timelimit="w", max_results=5)
|
| 20 |
+
# _duckduckgo_search returns list[dict], convert to str
|
| 21 |
+
results = _duckduckgo_search(params)
|
| 22 |
+
return str(results)
|
| 23 |
+
|
| 24 |
+
news_tool = FunctionTool(news_search, description="Search for latest top 5 news for a given stock or topic. Returns headlines and snippets only.")
|
| 25 |
+
|
| 26 |
+
news_agent = AssistantAgent(
|
| 27 |
+
name="news_agent",
|
| 28 |
+
model_client=model_client,
|
| 29 |
+
tools=[news_tool],
|
| 30 |
+
system_message=(
|
| 31 |
+
"You are the News Agent. "
|
| 32 |
+
"1. Search for the latest top 5 news stories related to the given stock using `news_tool`. "
|
| 33 |
+
"2. Prioritize reputable result sources like Bloomberg, Reuters, CNBC, WSJ, and Yahoo Finance if possible. "
|
| 34 |
+
"3. Summarize the key insights from the news stories. "
|
| 35 |
+
"Do NOT provide any final investment decision."
|
| 36 |
+
)
|
| 37 |
+
)
|
| 38 |
+
return news_agent
|
src/stock-investment-analyst/aagents/sentiment_agent.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autogen_agentchat.agents import AssistantAgent
|
| 2 |
+
from autogen_core.tools import FunctionTool
|
| 3 |
+
from tools.yf_tools import _get_analyst_recommendations
|
| 4 |
+
from core.model import get_model_client
|
| 5 |
+
|
| 6 |
+
def get_sentiment_agent():
|
| 7 |
+
model_client = get_model_client()
|
| 8 |
+
|
| 9 |
+
async def get_market_sentiment(symbol: str, period: str) -> str:
|
| 10 |
+
"""Get market sentiment for a stock."""
|
| 11 |
+
from tools.yf_tools import _get_market_sentiment
|
| 12 |
+
return _get_market_sentiment(symbol, period)
|
| 13 |
+
|
| 14 |
+
async def get_analyst_recs(symbol: str) -> str:
|
| 15 |
+
"""Get analyst recommendations for a stock."""
|
| 16 |
+
# _get_analyst_recommendations is already imported
|
| 17 |
+
return _get_analyst_recommendations(symbol)
|
| 18 |
+
|
| 19 |
+
sentiment_tool = FunctionTool(get_market_sentiment, description="Get market sentiment")
|
| 20 |
+
analyst_tool = FunctionTool(get_analyst_recs, description="Get analyst recommendations")
|
| 21 |
+
|
| 22 |
+
sentiment_agent = AssistantAgent(
|
| 23 |
+
name="sentiment_agent",
|
| 24 |
+
model_client=model_client,
|
| 25 |
+
tools=[sentiment_tool, analyst_tool],
|
| 26 |
+
system_message=(
|
| 27 |
+
"You are the Market Sentiment Agent. "
|
| 28 |
+
"You gather overall market sentiment, relevant analyst reports, and expert opinions. "
|
| 29 |
+
"Do NOT provide any final investment decision."
|
| 30 |
+
)
|
| 31 |
+
)
|
| 32 |
+
return sentiment_agent
|
src/stock-investment-analyst/aagents/stock_trends_agent.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autogen_agentchat.agents import AssistantAgent
|
| 2 |
+
from autogen_core.tools import FunctionTool
|
| 3 |
+
from tools.yf_tools import _get_history
|
| 4 |
+
from core.model import get_model_client
|
| 5 |
+
|
| 6 |
+
def get_stock_trends_agent():
|
| 7 |
+
model_client = get_model_client()
|
| 8 |
+
|
| 9 |
+
async def fetch_stock_history(symbol: str, period: str) -> str:
|
| 10 |
+
"""
|
| 11 |
+
Gets real-time stock prices and changes over the last few months for the given stock name.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
symbol: The stock ticker symbol (e.g., 'TSLA', 'AAPL').
|
| 15 |
+
period: The period to fetch data for (e.g., '1mo', '3mo').
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
str: A formatted string showing the historical prices.
|
| 19 |
+
"""
|
| 20 |
+
return _get_history(symbol, period)
|
| 21 |
+
|
| 22 |
+
get_history_tool = FunctionTool(fetch_stock_history, description="Gets real-time stock prices, changes over the last few months for 'stock_name'", strict=True)
|
| 23 |
+
|
| 24 |
+
stock_trends_agent_assistant = AssistantAgent(
|
| 25 |
+
name="stock_trends_agent",
|
| 26 |
+
model_client=model_client,
|
| 27 |
+
tools=[get_history_tool],
|
| 28 |
+
system_message=(
|
| 29 |
+
"You are the Stock Price Trends Agent practicing in India and USA stock markets. "
|
| 30 |
+
"You fetch and summarize stock prices, changes over the last 3 months, and general market trends. "
|
| 31 |
+
"Do NOT provide any final investment decision."
|
| 32 |
+
),
|
| 33 |
+
)
|
| 34 |
+
return stock_trends_agent_assistant
|
src/stock-investment-analyst/app.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
from teams.investment_team import get_investment_team
|
| 9 |
+
|
| 10 |
+
st.set_page_config(page_title="Stock Investment Analyst", layout="wide", page_icon="📈")
|
| 11 |
+
|
| 12 |
+
# ------------------------------------------------------------------------------
|
| 13 |
+
# Custom CSS for layout improvements
|
| 14 |
+
# ------------------------------------------------------------------------------
|
| 15 |
+
st.markdown("""
|
| 16 |
+
<style>
|
| 17 |
+
.block-container {
|
| 18 |
+
padding-top: 1.5rem;
|
| 19 |
+
padding-bottom: 3rem;
|
| 20 |
+
}
|
| 21 |
+
</style>
|
| 22 |
+
""", unsafe_allow_html=True)
|
| 23 |
+
|
| 24 |
+
# ------------------------------------------------------------------------------
|
| 25 |
+
# Main Content
|
| 26 |
+
# ------------------------------------------------------------------------------
|
| 27 |
+
st.title("📈 Stock Investment Analyst")
|
| 28 |
+
|
| 29 |
+
# IMPORTANT DISCLAIMER
|
| 30 |
+
st.warning(
|
| 31 |
+
"**⚠️ DISCLAIMER: EDUCATIONAL PURPOSE ONLY**\n\n"
|
| 32 |
+
"This tool is an AI-powered experiment designed for educational and demonstration purposes. "
|
| 33 |
+
"The analysis provided is generated by artificial intelligence and **DOES NOT** constitute financial, investment, or legal advice. "
|
| 34 |
+
"Do not use this tool to make real-world financial decisions. Always consult with a qualified financial advisor."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
st.markdown("#### Get a comprehensive analysis from our AI team of agents.")
|
| 38 |
+
|
| 39 |
+
# Input section with better spacing
|
| 40 |
+
st.divider()
|
| 41 |
+
with st.container():
|
| 42 |
+
col1, col2 = st.columns([3, 1], gap="medium")
|
| 43 |
+
with col1:
|
| 44 |
+
stock_name = st.text_input(
|
| 45 |
+
"Enter Stock Ticker",
|
| 46 |
+
value="Tesla",
|
| 47 |
+
placeholder="e.g. NVDA, TSLA, AAPL",
|
| 48 |
+
help="Enter the ticker symbol of the company you want to analyze."
|
| 49 |
+
)
|
| 50 |
+
with col2:
|
| 51 |
+
# Align button with input box
|
| 52 |
+
st.write("") # Spacer
|
| 53 |
+
st.write("") # Spacer
|
| 54 |
+
analyze_btn = st.button("🔍 Analyze Stock", type="primary", use_container_width=True)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
async def run_analysis(ticker):
|
| 58 |
+
# Start a span for the analysis task.
|
| 59 |
+
# This becomes the parent for all subsequent spans (like OpenAI calls).
|
| 60 |
+
# with tracer.start_as_current_span("run_analysis") as span:
|
| 61 |
+
# span.set_attribute("stock.ticker", ticker) # Add useful metadata
|
| 62 |
+
|
| 63 |
+
task = f"Analyze stock trends, news, and sentiment for {ticker}, plus analyst reports and expert opinions, and then decide whether to invest."
|
| 64 |
+
|
| 65 |
+
st.markdown(f"### Analysis for **{ticker}**")
|
| 66 |
+
|
| 67 |
+
# Container for live updates
|
| 68 |
+
chat_container = st.container()
|
| 69 |
+
|
| 70 |
+
try:
|
| 71 |
+
# Run the team stream
|
| 72 |
+
investment_team = get_investment_team()
|
| 73 |
+
stream = investment_team.run_stream(task=task)
|
| 74 |
+
|
| 75 |
+
# Define icons for each agent
|
| 76 |
+
AGENT_ICONS = {
|
| 77 |
+
"stock_trends_agent": "📈",
|
| 78 |
+
"news_agent": "📰",
|
| 79 |
+
"sentiment_agent": "💡",
|
| 80 |
+
"decision_agent": "⚖️",
|
| 81 |
+
"user": "👤",
|
| 82 |
+
"System": "⚙️"
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
async for message in stream:
|
| 86 |
+
# Check if message has source and content attributes typical of agent messages
|
| 87 |
+
source = getattr(message, 'source', 'System')
|
| 88 |
+
|
| 89 |
+
with chat_container:
|
| 90 |
+
if 'TaskResult' in message.__class__.__name__:
|
| 91 |
+
if hasattr(message, 'stop_reason') and message.stop_reason:
|
| 92 |
+
st.info(f"Analysis Completed: {message.stop_reason}")
|
| 93 |
+
continue
|
| 94 |
+
|
| 95 |
+
# Use the icon mapping, default to None (Streamlit default) if not found
|
| 96 |
+
avatar = AGENT_ICONS.get(source, None)
|
| 97 |
+
|
| 98 |
+
with st.chat_message(source, avatar=avatar):
|
| 99 |
+
# Handle Tool Call events specifically to make them look like system logs
|
| 100 |
+
if 'ToolCall' in message.__class__.__name__:
|
| 101 |
+
with st.expander(f"⚙️ Tool Usage: {source}", expanded=False):
|
| 102 |
+
st.write(message)
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
content = getattr(message, 'content', "")
|
| 106 |
+
st.write(content)
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
# Record the exception in the span if something crashes
|
| 110 |
+
# span.record_exception(e)
|
| 111 |
+
# span.set_status(trace.Status(trace.StatusCode.ERROR))
|
| 112 |
+
st.error(f"An error occurred during analysis: {e}")
|
| 113 |
+
|
| 114 |
+
if analyze_btn:
|
| 115 |
+
if stock_name:
|
| 116 |
+
with st.spinner(f"Gathering data and analyzing {stock_name}..."):
|
| 117 |
+
# Create a new event loop for this run if needed, or simply run
|
| 118 |
+
asyncio.run(run_analysis(stock_name))
|
| 119 |
+
else:
|
| 120 |
+
st.warning("Please enter a valid stock ticker.")
|
src/stock-investment-analyst/core/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
from .model import get_model_client
|
| 3 |
+
|
| 4 |
+
__all__ = ["get_model_client"]
|
src/stock-investment-analyst/core/model.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from utility.autogen_model_factory import AutoGenModelFactory
|
| 2 |
+
|
| 3 |
+
def get_model_client(provider:str = "google"):
|
| 4 |
+
if provider.lower() == "google":
|
| 5 |
+
return AutoGenModelFactory.get_model(
|
| 6 |
+
provider="google",
|
| 7 |
+
model_name="gemini-3-pro-preview",
|
| 8 |
+
temperature=0,
|
| 9 |
+
model_info={
|
| 10 |
+
"family": "gemini",
|
| 11 |
+
"vision": True,
|
| 12 |
+
"function_calling": True,
|
| 13 |
+
"json_output": True,
|
| 14 |
+
"structured_output": True,
|
| 15 |
+
},
|
| 16 |
+
)
|
| 17 |
+
elif provider.lower() == "openai":
|
| 18 |
+
return AutoGenModelFactory.get_model(
|
| 19 |
+
provider="openai",
|
| 20 |
+
model_name="gpt-4o-mini",
|
| 21 |
+
temperature=0,
|
| 22 |
+
model_info={
|
| 23 |
+
"family": "gpt",
|
| 24 |
+
"vision": True,
|
| 25 |
+
"function_calling": True,
|
| 26 |
+
"json_output": True,
|
| 27 |
+
"structured_output": True,
|
| 28 |
+
},
|
| 29 |
+
)
|
| 30 |
+
else:
|
| 31 |
+
raise ValueError(f"Unsupported provider: {provider}")
|
| 32 |
+
|
src/stock-investment-analyst/teams/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .investment_team import get_investment_team
|
| 2 |
+
|
| 3 |
+
__all__ = ["get_investment_team"]
|
src/stock-investment-analyst/teams/investment_team.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination
|
| 2 |
+
from autogen_agentchat.teams import SelectorGroupChat
|
| 3 |
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
| 4 |
+
from autogen_agentchat.ui import Console
|
| 5 |
+
import os
|
| 6 |
+
|
| 7 |
+
from aagents import get_stock_trends_agent, get_news_agent, get_sentiment_agent, get_decision_agent, StockTrend
|
| 8 |
+
from autogen_agentchat.messages import StructuredMessage
|
| 9 |
+
from core.model import get_model_client
|
| 10 |
+
|
| 11 |
+
def get_investment_team():
|
| 12 |
+
# Try to register the message type to avoid "not registered" errors in GroupChat
|
| 13 |
+
try:
|
| 14 |
+
from autogen_core import TypeSubscription
|
| 15 |
+
pass
|
| 16 |
+
except ImportError:
|
| 17 |
+
pass
|
| 18 |
+
|
| 19 |
+
text_termination = TextMentionTermination("Decision Made")
|
| 20 |
+
max_message_termination = MaxMessageTermination(20)
|
| 21 |
+
termination = text_termination | max_message_termination
|
| 22 |
+
|
| 23 |
+
# # Model for the selector/moderator
|
| 24 |
+
# selector_model = OpenAIChatCompletionClient(
|
| 25 |
+
# model="gemini-2.5-flash",
|
| 26 |
+
# api_key=os.getenv("GOOGLE_API_KEY"),
|
| 27 |
+
# model_info={
|
| 28 |
+
# "family": "gemini",
|
| 29 |
+
# "vision": True,
|
| 30 |
+
# "function_calling": True,
|
| 31 |
+
# "json_output": True,
|
| 32 |
+
# "structured_output": True,
|
| 33 |
+
# },
|
| 34 |
+
# temperature=0
|
| 35 |
+
# )
|
| 36 |
+
|
| 37 |
+
# Selector Group Chat which allows dynamic speaker selection
|
| 38 |
+
investment_team = SelectorGroupChat(
|
| 39 |
+
[
|
| 40 |
+
get_stock_trends_agent(),
|
| 41 |
+
get_news_agent(),
|
| 42 |
+
get_sentiment_agent(),
|
| 43 |
+
get_decision_agent(),
|
| 44 |
+
],
|
| 45 |
+
model_client=get_model_client(),
|
| 46 |
+
termination_condition=termination
|
| 47 |
+
)
|
| 48 |
+
return investment_team
|
| 49 |
+
|
src/stock-investment-analyst/tools/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .search_tools import _duckduckgo_search, searchQuery
|
| 2 |
+
from .yf_tools import _get_history, _get_analyst_recommendations
|
| 3 |
+
|
| 4 |
+
__all__ = ["_duckduckgo_search", "searchQuery", "_get_history", "_get_analyst_recommendations"]
|
src/stock-investment-analyst/tools/search_tools.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from ddgs import DDGS
|
| 3 |
+
from agents import function_tool
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from bs4 import BeautifulSoup
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
# ---------------------------------------------------------
|
| 12 |
+
# Load environment variables
|
| 13 |
+
# ---------------------------------------------------------
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# ---------------------- MODELS ---------------------------
|
| 17 |
+
class searchQuery(BaseModel):
|
| 18 |
+
query: str = Field(..., description="The search query string.")
|
| 19 |
+
max_results: int = Field(5, description="The maximum number of search results to return.")
|
| 20 |
+
search_type: str = Field(
|
| 21 |
+
"text",
|
| 22 |
+
description="Search type: 'text' (default) or 'news'. Use 'news' to get publication dates."
|
| 23 |
+
)
|
| 24 |
+
timelimit: str = Field(
|
| 25 |
+
'd',
|
| 26 |
+
description="Time limit for search results: 'd' (day), 'w' (week), 'm' (month), 'y' (year)."
|
| 27 |
+
)
|
| 28 |
+
region: str = Field("us-en", description="Region for search results (e.g., 'us-en').")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class searchResult(BaseModel):
|
| 32 |
+
title: str
|
| 33 |
+
link: str
|
| 34 |
+
snippet: str
|
| 35 |
+
datetime: Optional[str] = None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# ---------------------- PAGE FETCH TOOL ---------------------------
|
| 39 |
+
def _fetch_page_content(url: str, timeout: int = 3) -> Optional[str]:
|
| 40 |
+
"""Fetch and extract text content from a web page."""
|
| 41 |
+
print(f"[DEBUG] fetch_page_content called with: {url} - timeout: {timeout}")
|
| 42 |
+
try:
|
| 43 |
+
headers = {
|
| 44 |
+
'User-Agent': (
|
| 45 |
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
|
| 46 |
+
'AppleWebKit/537.36 (KHTML, like Gecko) '
|
| 47 |
+
'Chrome/91.0.4472.124 Safari/537.36'
|
| 48 |
+
)
|
| 49 |
+
}
|
| 50 |
+
response = requests.get(url, headers=headers, timeout=timeout)
|
| 51 |
+
response.raise_for_status()
|
| 52 |
+
|
| 53 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 54 |
+
|
| 55 |
+
# Remove irrelevant elements
|
| 56 |
+
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
| 57 |
+
tag.decompose()
|
| 58 |
+
|
| 59 |
+
# Extract text
|
| 60 |
+
text = soup.get_text(separator='\n', strip=True)
|
| 61 |
+
|
| 62 |
+
# Clean whitespace
|
| 63 |
+
lines = (line.strip() for line in text.splitlines())
|
| 64 |
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
| 65 |
+
text = '\n'.join(chunk for chunk in chunks if chunk)
|
| 66 |
+
|
| 67 |
+
return text
|
| 68 |
+
except Exception as e:
|
| 69 |
+
print(f"[WARNING] Failed to fetch content from {url}: {str(e)}")
|
| 70 |
+
return None
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@function_tool
|
| 74 |
+
def fetch_page_content(url: str, timeout: int = 3) -> Optional[str]:
|
| 75 |
+
"""Fetch and extract text content from a web page."""
|
| 76 |
+
return _fetch_page_content(url, timeout)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ---------------------- SEARCH TOOL ---------------------------
|
| 80 |
+
def _duckduckgo_search(params: searchQuery) -> list[dict]:
|
| 81 |
+
"""Perform a DuckDuckGo search and return only snippets.
|
| 82 |
+
No page content fetched here."""
|
| 83 |
+
print(f"[DEBUG] duckduckgo_search called with: {params}")
|
| 84 |
+
|
| 85 |
+
results = []
|
| 86 |
+
with DDGS() as ddgs:
|
| 87 |
+
if params.search_type == "news":
|
| 88 |
+
search_results = ddgs.news(
|
| 89 |
+
params.query,
|
| 90 |
+
max_results=params.max_results,
|
| 91 |
+
timelimit=params.timelimit,
|
| 92 |
+
region=params.region
|
| 93 |
+
)
|
| 94 |
+
for result in search_results:
|
| 95 |
+
results.append(
|
| 96 |
+
searchResult(
|
| 97 |
+
title=result.get("title", ""),
|
| 98 |
+
link=result.get("url", ""),
|
| 99 |
+
snippet=result.get("body", ""),
|
| 100 |
+
datetime=result.get("date", "")
|
| 101 |
+
).model_dump()
|
| 102 |
+
)
|
| 103 |
+
else:
|
| 104 |
+
search_results = ddgs.text(
|
| 105 |
+
params.query,
|
| 106 |
+
max_results=params.max_results,
|
| 107 |
+
timelimit=params.timelimit,
|
| 108 |
+
region=params.region
|
| 109 |
+
)
|
| 110 |
+
for result in search_results:
|
| 111 |
+
results.append(
|
| 112 |
+
searchResult(
|
| 113 |
+
title=result.get("title", ""),
|
| 114 |
+
link=result.get("href", ""),
|
| 115 |
+
snippet=result.get("body", "")
|
| 116 |
+
).model_dump()
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
print(f"[DEBUG] duckduckgo_search returning {len(results)} results")
|
| 120 |
+
return results
|
| 121 |
+
|
| 122 |
+
@function_tool
|
| 123 |
+
def duckduckgo_search(params: searchQuery) -> list[dict]:
|
| 124 |
+
"""Perform a DuckDuckGo search and return only snippets.
|
| 125 |
+
No page content fetched here."""
|
| 126 |
+
return _duckduckgo_search(params)
|
| 127 |
+
|
src/stock-investment-analyst/tools/yf_tools.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
import yfinance as yf
|
| 4 |
+
|
| 5 |
+
from agents import function_tool
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Load environment variables
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# ============================================================
|
| 15 |
+
# 🔹 YAHOO FINANCE TOOLSET
|
| 16 |
+
# ============================================================
|
| 17 |
+
def _get_summary(symbol: str, period: str = "1d", interval: str = "1h") -> str:
|
| 18 |
+
print(f"[DEBUG] get_summary called for symbol='{symbol}', period='{period}', interval='{interval}'")
|
| 19 |
+
try:
|
| 20 |
+
ticker = yf.Ticker(symbol)
|
| 21 |
+
|
| 22 |
+
# Calculate start and end dates based on period
|
| 23 |
+
end_date = datetime.today()
|
| 24 |
+
if period.endswith("d"):
|
| 25 |
+
days = int(period[:-1])
|
| 26 |
+
elif period.endswith("mo"):
|
| 27 |
+
days = int(period[:-2]) * 30
|
| 28 |
+
elif period.endswith("y"):
|
| 29 |
+
days = int(period[:-1]) * 365
|
| 30 |
+
else:
|
| 31 |
+
days = 30 # default 1 month
|
| 32 |
+
start_date = end_date - timedelta(days=days)
|
| 33 |
+
|
| 34 |
+
# Fetch recent data explicitly
|
| 35 |
+
data = ticker.history(
|
| 36 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 37 |
+
end=end_date.strftime("%Y-%m-%d"),
|
| 38 |
+
interval=interval
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
if data.empty:
|
| 42 |
+
return f"No data found for symbol '{symbol}'."
|
| 43 |
+
|
| 44 |
+
latest = data.iloc[-1]
|
| 45 |
+
current_price = round(latest["Close"], 2)
|
| 46 |
+
open_price = round(latest["Open"], 2)
|
| 47 |
+
change = round(current_price - open_price, 2)
|
| 48 |
+
pct_change = round((change / open_price) * 100, 2)
|
| 49 |
+
|
| 50 |
+
info = ticker.info
|
| 51 |
+
long_name = info.get("longName", symbol)
|
| 52 |
+
currency = info.get("currency", "USD")
|
| 53 |
+
|
| 54 |
+
formatted = [
|
| 55 |
+
f"📈 {long_name} ({symbol})",
|
| 56 |
+
f"Current Price: {current_price} {currency}",
|
| 57 |
+
f"Change: {change} ({pct_change}%)",
|
| 58 |
+
f"Open: {open_price} | High: {round(latest['High'], 2)} | Low: {round(latest['Low'], 2)}",
|
| 59 |
+
f"Volume: {int(latest['Volume'])}",
|
| 60 |
+
f"Period: {period} | Interval: {interval}",
|
| 61 |
+
]
|
| 62 |
+
return "\n".join(formatted)
|
| 63 |
+
|
| 64 |
+
except Exception as e:
|
| 65 |
+
return f"Error fetching data for '{symbol}': {e}"
|
| 66 |
+
|
| 67 |
+
def _get_market_sentiment(symbol: str, period: str = "1mo") -> str:
|
| 68 |
+
print(f"[DEBUG] get_market_sentiment called for symbol='{symbol}', period='{period}'")
|
| 69 |
+
try:
|
| 70 |
+
ticker = yf.Ticker(symbol)
|
| 71 |
+
|
| 72 |
+
# Calculate start/end dynamically
|
| 73 |
+
end_date = datetime.today()
|
| 74 |
+
if period.endswith("d"):
|
| 75 |
+
days = int(period[:-1])
|
| 76 |
+
elif period.endswith("mo"):
|
| 77 |
+
days = int(period[:-2]) * 30
|
| 78 |
+
elif period.endswith("y"):
|
| 79 |
+
days = int(period[:-1]) * 365
|
| 80 |
+
else:
|
| 81 |
+
days = 30
|
| 82 |
+
start_date = end_date - timedelta(days=days)
|
| 83 |
+
|
| 84 |
+
data = ticker.history(
|
| 85 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 86 |
+
end=end_date.strftime("%Y-%m-%d")
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
if data.empty:
|
| 90 |
+
return f"No data for {symbol}."
|
| 91 |
+
|
| 92 |
+
recent_change = data["Close"].iloc[-1] - data["Close"].iloc[0]
|
| 93 |
+
pct_change = (recent_change / data["Close"].iloc[0]) * 100
|
| 94 |
+
|
| 95 |
+
sentiment = "Neutral"
|
| 96 |
+
if pct_change > 2:
|
| 97 |
+
sentiment = "Bullish"
|
| 98 |
+
elif pct_change < -2:
|
| 99 |
+
sentiment = "Bearish"
|
| 100 |
+
|
| 101 |
+
return f"{symbol} market sentiment ({period}): {sentiment} ({pct_change:.2f}% change)"
|
| 102 |
+
|
| 103 |
+
except Exception as e:
|
| 104 |
+
return f"Error fetching market sentiment for '{symbol}': {e}"
|
| 105 |
+
|
| 106 |
+
def _get_history(symbol: str, period: str = "1mo") -> str:
|
| 107 |
+
print(f"[DEBUG] get_history called for symbol='{symbol}', period='{period}'")
|
| 108 |
+
try:
|
| 109 |
+
ticker = yf.Ticker(symbol)
|
| 110 |
+
|
| 111 |
+
# Calculate start/end dynamically
|
| 112 |
+
end_date = datetime.today()
|
| 113 |
+
if period.endswith("d"):
|
| 114 |
+
days = int(period[:-1])
|
| 115 |
+
elif period.endswith("mo"):
|
| 116 |
+
days = int(period[:-2]) * 30
|
| 117 |
+
elif period.endswith("y"):
|
| 118 |
+
days = int(period[:-1]) * 365
|
| 119 |
+
else:
|
| 120 |
+
days = 30
|
| 121 |
+
start_date = end_date - timedelta(days=days)
|
| 122 |
+
|
| 123 |
+
data = ticker.history(
|
| 124 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 125 |
+
end=end_date.strftime("%Y-%m-%d")
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if data.empty:
|
| 129 |
+
return f"No historical data found for '{symbol}'."
|
| 130 |
+
|
| 131 |
+
# Convert to JSON format (reset index to include Date)
|
| 132 |
+
# return data.tail(5).reset_index().to_json(orient='records', date_format='iso')
|
| 133 |
+
return f"Historical data for {symbol} ({period}):\n{data.tail(5).to_string()}"
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
return f"Error fetching historical data for '{symbol}': {e}"
|
| 137 |
+
|
| 138 |
+
def _get_analyst_recommendations(symbol: str) -> str:
|
| 139 |
+
print(f"[DEBUG] get_analyst_recommendations called for symbol='{symbol}'")
|
| 140 |
+
try:
|
| 141 |
+
ticker = yf.Ticker(symbol)
|
| 142 |
+
recs = ticker.recommendations
|
| 143 |
+
if recs is None or recs.empty:
|
| 144 |
+
return f"No analyst recommendations found for {symbol}."
|
| 145 |
+
|
| 146 |
+
# Format the last few recommendations
|
| 147 |
+
latest = recs.tail(5)
|
| 148 |
+
return f"Analyst Recommendations for {symbol}:\n{latest.to_string()}"
|
| 149 |
+
except Exception as e:
|
| 150 |
+
return f"Error fetching recommendations for '{symbol}': {e}"
|
| 151 |
+
|
| 152 |
+
def _get_earnings_calendar(symbol: str) -> str:
|
| 153 |
+
print(f"[DEBUG] get_earnings_calendar called for symbol='{symbol}'")
|
| 154 |
+
try:
|
| 155 |
+
ticker = yf.Ticker(symbol)
|
| 156 |
+
calendar = ticker.calendar
|
| 157 |
+
if calendar is None:
|
| 158 |
+
return f"No earnings calendar found for {symbol}."
|
| 159 |
+
|
| 160 |
+
# Handle dict (new yfinance) or DataFrame (old yfinance)
|
| 161 |
+
if isinstance(calendar, dict):
|
| 162 |
+
if not calendar:
|
| 163 |
+
return f"No earnings calendar found for {symbol}."
|
| 164 |
+
elif hasattr(calendar, 'empty') and calendar.empty:
|
| 165 |
+
return f"No earnings calendar found for {symbol}."
|
| 166 |
+
|
| 167 |
+
return f"Earnings Calendar for {symbol}:\n{calendar}"
|
| 168 |
+
except Exception as e:
|
| 169 |
+
return f"Error fetching earnings calendar for '{symbol}': {e}"
|
| 170 |
+
|
| 171 |
+
@function_tool
|
| 172 |
+
def get_summary(symbol: str, period: str = "1d", interval: str = "1h") -> str:
|
| 173 |
+
"""
|
| 174 |
+
Fetch the latest summary information and intraday price data for a given ticker.
|
| 175 |
+
Ensures recent data is retrieved by calculating start/end dates dynamically.
|
| 176 |
+
|
| 177 |
+
Parameters:
|
| 178 |
+
-----------
|
| 179 |
+
symbol : str
|
| 180 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 181 |
+
period : str, optional (default="1d")
|
| 182 |
+
Time range for price data. Examples: "1d", "5d", "1mo", "3mo".
|
| 183 |
+
interval : str, optional (default="1h")
|
| 184 |
+
Granularity of the data. Examples: "1m", "5m", "1h", "1d".
|
| 185 |
+
|
| 186 |
+
Returns:
|
| 187 |
+
--------
|
| 188 |
+
str
|
| 189 |
+
A formatted string containing:
|
| 190 |
+
- Company/ticker name
|
| 191 |
+
- Current price and change
|
| 192 |
+
- Open, High, Low prices
|
| 193 |
+
- Volume
|
| 194 |
+
- Period and interval used
|
| 195 |
+
"""
|
| 196 |
+
return _get_summary(symbol, period, interval)
|
| 197 |
+
|
| 198 |
+
@function_tool
|
| 199 |
+
def get_market_sentiment(symbol: str, period: str = "1mo") -> str:
|
| 200 |
+
"""
|
| 201 |
+
Analyze recent price changes and provide a simple market sentiment.
|
| 202 |
+
Uses dynamic start/end dates to ensure recent data.
|
| 203 |
+
|
| 204 |
+
This tool computes the percentage change over the specified period and
|
| 205 |
+
classifies the sentiment as:
|
| 206 |
+
- Bullish (if price increased >2%)
|
| 207 |
+
- Bearish (if price decreased >2%)
|
| 208 |
+
- Neutral (otherwise)
|
| 209 |
+
|
| 210 |
+
Parameters:
|
| 211 |
+
-----------
|
| 212 |
+
symbol : str
|
| 213 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 214 |
+
period : str, optional (default="1mo")
|
| 215 |
+
Time range to analyze. Examples: "7d", "1mo", "3mo".
|
| 216 |
+
|
| 217 |
+
Returns:
|
| 218 |
+
--------
|
| 219 |
+
str
|
| 220 |
+
A human-readable sentiment string including percentage change.
|
| 221 |
+
"""
|
| 222 |
+
return _get_market_sentiment(symbol, period)
|
| 223 |
+
|
| 224 |
+
@function_tool
|
| 225 |
+
def get_history(symbol: str, period: str = "1mo") -> str:
|
| 226 |
+
"""
|
| 227 |
+
Fetch historical price data for a given ticker.
|
| 228 |
+
Ensures recent data is retrieved dynamically using start/end dates.
|
| 229 |
+
|
| 230 |
+
Parameters:
|
| 231 |
+
-----------
|
| 232 |
+
symbol : str
|
| 233 |
+
The ticker symbol (e.g., "AAPL", "GOOG", "BTC-USD").
|
| 234 |
+
period : str, optional (default="1mo")
|
| 235 |
+
The length of historical data to retrieve. Examples: "1d", "5d", "1mo", "3mo", "1y", "5y".
|
| 236 |
+
|
| 237 |
+
Returns:
|
| 238 |
+
--------
|
| 239 |
+
str
|
| 240 |
+
A formatted string showing the last 5 rows of historical prices (Open, High, Low, Close, Volume).
|
| 241 |
+
"""
|
| 242 |
+
return _get_history(symbol, period)
|
| 243 |
+
|
| 244 |
+
@function_tool
|
| 245 |
+
def get_analyst_recommendations(symbol: str) -> str:
|
| 246 |
+
"""
|
| 247 |
+
Fetch analyst recommendations for a given ticker.
|
| 248 |
+
|
| 249 |
+
Parameters:
|
| 250 |
+
-----------
|
| 251 |
+
symbol : str
|
| 252 |
+
The ticker symbol.
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
--------
|
| 256 |
+
str
|
| 257 |
+
Formatted string string of analyst recommendations.
|
| 258 |
+
"""
|
| 259 |
+
return _get_analyst_recommendations(symbol)
|
| 260 |
+
|
| 261 |
+
@function_tool
|
| 262 |
+
def get_earnings_calendar(symbol: str) -> str:
|
| 263 |
+
"""
|
| 264 |
+
Fetch the next earnings date for a ticker.
|
| 265 |
+
|
| 266 |
+
Parameters:
|
| 267 |
+
-----------
|
| 268 |
+
symbol : str
|
| 269 |
+
The ticker symbol.
|
| 270 |
+
|
| 271 |
+
Returns:
|
| 272 |
+
--------
|
| 273 |
+
str
|
| 274 |
+
Next earnings date info.
|
| 275 |
+
"""
|
| 276 |
+
return _get_earnings_calendar(symbol)
|
| 277 |
+
|
src/stock-investment-analyst/utility/__init__.py
ADDED
|
File without changes
|
src/stock-investment-analyst/utility/autogen_model_factory.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
|
| 3 |
+
|
| 4 |
+
class AutoGenModelFactory:
|
| 5 |
+
"""
|
| 6 |
+
Factory for creating AutoGen compatible model instances.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
@staticmethod
|
| 10 |
+
def get_model(provider: str = "azure", # azure, openai, google, groq, ollama
|
| 11 |
+
model_name: str = "gpt-4o",
|
| 12 |
+
temperature: float = 0,
|
| 13 |
+
model_info: dict = None
|
| 14 |
+
):
|
| 15 |
+
"""
|
| 16 |
+
Returns an AutoGen OpenAIChatCompletionClient instance.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
# Lazy import to avoid dependency issues if autogen is not installed
|
| 20 |
+
try:
|
| 21 |
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
| 22 |
+
except ImportError as e:
|
| 23 |
+
raise ImportError("AutoGen libraries (autogen-agentchat, autogen-ext[openai]) are not installed.") from e
|
| 24 |
+
|
| 25 |
+
# ----------------------------------------------------------------------
|
| 26 |
+
# AZURE
|
| 27 |
+
# ----------------------------------------------------------------------
|
| 28 |
+
if provider.lower() == "azure":
|
| 29 |
+
token_provider = get_bearer_token_provider(
|
| 30 |
+
DefaultAzureCredential(), "https://cognitiveservices.azure.com/.default"
|
| 31 |
+
)
|
| 32 |
+
return OpenAIChatCompletionClient(
|
| 33 |
+
model=model_name,
|
| 34 |
+
azure_endpoint=os.environ["AZURE_OPENAI_API_URI"],
|
| 35 |
+
api_version=os.environ["AZURE_OPENAI_API_VERSION"],
|
| 36 |
+
azure_ad_token_provider=token_provider,
|
| 37 |
+
temperature=temperature,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# ----------------------------------------------------------------------
|
| 41 |
+
# OPENAI
|
| 42 |
+
# ----------------------------------------------------------------------
|
| 43 |
+
elif provider.lower() == "openai":
|
| 44 |
+
return OpenAIChatCompletionClient(
|
| 45 |
+
model=model_name,
|
| 46 |
+
api_key=os.environ["OPENAI_API_KEY"],
|
| 47 |
+
temperature=temperature,
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# ----------------------------------------------------------------------
|
| 51 |
+
# GOOGLE (GEMINI) via OpenAI Compat
|
| 52 |
+
# ----------------------------------------------------------------------
|
| 53 |
+
elif provider.lower() == "google" or provider.lower() == "gemini":
|
| 54 |
+
if model_info is None:
|
| 55 |
+
model_info = {
|
| 56 |
+
"family": "gemini",
|
| 57 |
+
"vision": False,
|
| 58 |
+
"function_calling": True,
|
| 59 |
+
"json_output": True,
|
| 60 |
+
"structured_output": False
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return OpenAIChatCompletionClient(
|
| 64 |
+
model=model_name,
|
| 65 |
+
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
|
| 66 |
+
api_key=os.environ["GOOGLE_API_KEY"],
|
| 67 |
+
model_info=model_info,
|
| 68 |
+
temperature=temperature,
|
| 69 |
+
max_tokens=2048,
|
| 70 |
+
structured_output=False, # Disable for Gemini compatibility
|
| 71 |
+
extra_headers={"x-goog-api-key": os.environ["GOOGLE_API_KEY"]}
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# ----------------------------------------------------------------------
|
| 75 |
+
# GROQ
|
| 76 |
+
# ----------------------------------------------------------------------
|
| 77 |
+
elif provider.lower() == "groq":
|
| 78 |
+
if model_info is None:
|
| 79 |
+
model_info = {
|
| 80 |
+
"family": "llama", # Use llama family for Groq
|
| 81 |
+
"vision": False,
|
| 82 |
+
"function_calling": True,
|
| 83 |
+
"json_output": True,
|
| 84 |
+
"structured_output": False
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
return OpenAIChatCompletionClient(
|
| 88 |
+
model=model_name,
|
| 89 |
+
base_url="https://api.groq.com/openai/v1",
|
| 90 |
+
api_key=os.environ["GROQ_API_KEY"],
|
| 91 |
+
model_info=model_info,
|
| 92 |
+
temperature=temperature,
|
| 93 |
+
structured_output=False, # Disable for Groq compatibility
|
| 94 |
+
max_tokens=2048
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
# ----------------------------------------------------------------------
|
| 98 |
+
# OLLAMA
|
| 99 |
+
# ----------------------------------------------------------------------
|
| 100 |
+
elif provider.lower() == "ollama":
|
| 101 |
+
# Ensure model_info defaults to empty dict if None
|
| 102 |
+
info = model_info if model_info is not None else {}
|
| 103 |
+
return OpenAIChatCompletionClient(
|
| 104 |
+
model=model_name,
|
| 105 |
+
base_url="http://localhost:11434/v1",
|
| 106 |
+
api_key="ollama", # dummy key
|
| 107 |
+
model_info=info,
|
| 108 |
+
temperature=temperature,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
else:
|
| 112 |
+
raise ValueError(f"Unsupported AutoGen provider: {provider}")
|
uv.lock
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|