feat: Refactor services architecture and update project structure

- Remove Docker-based microservices (docker-compose.yml, Makefile, Dockerfiles)
- Update start-dev.sh to use backend.app:app entry point
- Add shared schema and client modules for service communication
- Add team coordination modules (messenger, registry, task_delegator, coordinator)
- Add evaluation hooks and skill adaptation hooks
- Add skill template and gateway server
- Update frontend WebSocket URL configuration
- Add explain components for insider and technical analysis

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 00:57:09 +08:00
parent 4b5ac86b83
commit 5b925fbe02
27 changed files with 4213 additions and 1 deletions

12
shared/client/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""Shared client package."""
from shared.client.trading_client import TradingServiceClient
from shared.client.news_client import NewsServiceClient
from shared.client.agent_client import AgentServiceClient
__all__ = [
"TradingServiceClient",
"NewsServiceClient",
"AgentServiceClient",
]

View File

@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
"""Agent service client for agent orchestration and runtime operations."""
import json
from typing import Any, AsyncIterator
import httpx
import websockets
from shared.schema.signals import AgentStateData
class AgentServiceClient:
"""Async client for the Agent Service API."""
def __init__(self, base_url: str = "http://localhost:8000"):
"""Initialize the client with a base URL.
Args:
base_url: Base URL for the agent service API.
"""
self.base_url = base_url.rstrip("/")
self._client: httpx.AsyncClient | None = None
async def __aenter__(self) -> "AgentServiceClient":
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
if self._client:
await self._client.aclose()
async def get_agents(self) -> dict:
"""Get list of all registered agents.
Returns:
Dictionary with agent list.
"""
response = await self._client.get("/api/agents")
response.raise_for_status()
return response.json()
async def get_agent_status(self, agent_id: str) -> dict:
"""Get status of a specific agent.
Args:
agent_id: The agent identifier.
Returns:
Dictionary with agent status.
"""
response = await self._client.get(f"/api/agents/{agent_id}/status")
response.raise_for_status()
return response.json()
async def post_run_daily(
self,
tickers: list[str],
start_date: str,
end_date: str,
runtime_config: dict[str, Any] | None = None,
) -> dict:
"""Trigger a daily analysis run.
Args:
tickers: List of stock tickers to analyze.
start_date: Start date (YYYY-MM-DD).
end_date: End date (YYYY-MM-DD).
runtime_config: Optional runtime configuration.
Returns:
Dictionary with run initiation response.
"""
payload = {
"tickers": tickers,
"start_date": start_date,
"end_date": end_date,
}
if runtime_config:
payload["runtime_config"] = runtime_config
response = await self._client.post("/api/run/daily", json=payload)
response.raise_for_status()
return response.json()
async def get_run_status(self, run_id: str) -> dict:
"""Get status of a run.
Args:
run_id: The run identifier.
Returns:
Dictionary with run status.
"""
response = await self._client.get(f"/api/runs/{run_id}/status")
response.raise_for_status()
return response.json()
async def get_run_result(self, run_id: str) -> AgentStateData:
"""Get the result of a completed run.
Args:
run_id: The run identifier.
Returns:
AgentStateData with run results.
"""
response = await self._client.get(f"/api/runs/{run_id}/result")
response.raise_for_status()
return AgentStateData.model_validate(response.json())
async def get_run_logs(self, run_id: str) -> dict:
"""Get logs for a run.
Args:
run_id: The run identifier.
Returns:
Dictionary with run logs.
"""
response = await self._client.get(f"/api/runs/{run_id}/logs")
response.raise_for_status()
return response.json()
async def cancel_run(self, run_id: str) -> dict:
"""Cancel a running task.
Args:
run_id: The run identifier.
Returns:
Dictionary with cancellation confirmation.
"""
response = await self._client.post(f"/api/runs/{run_id}/cancel")
response.raise_for_status()
return response.json()
async def get_runtime_config(self) -> dict:
"""Get current runtime configuration.
Returns:
Dictionary with runtime config.
"""
response = await self._client.get("/api/runtime/config")
response.raise_for_status()
return response.json()
async def update_runtime_config(self, config: dict[str, Any]) -> dict:
"""Update runtime configuration.
Args:
config: New runtime configuration.
Returns:
Dictionary with updated config.
"""
response = await self._client.put("/api/runtime/config", json=config)
response.raise_for_status()
return response.json()
async def websocket_connect(
self,
run_id: str | None = None,
) -> AsyncIterator[dict]:
"""Connect to WebSocket for real-time updates.
Args:
run_id: Optional run ID to subscribe to.
Yields:
Dictionary with WebSocket messages.
"""
ws_url = self.base_url.replace("http", "ws") + "/ws"
if run_id:
ws_url += f"?run_id={run_id}"
async with websockets.connect(ws_url) as ws:
async for message in ws:
yield json.loads(message)
async def get_pipeline_status(self) -> dict:
"""Get current pipeline execution status.
Returns:
Dictionary with pipeline status.
"""
response = await self._client.get("/api/pipeline/status")
response.raise_for_status()
return response.json()
async def trigger_pipeline(
self,
pipeline_type: str,
tickers: list[str],
config: dict[str, Any] | None = None,
) -> dict:
"""Trigger a pipeline execution.
Args:
pipeline_type: Type of pipeline to run.
tickers: List of tickers to process.
config: Optional pipeline configuration.
Returns:
Dictionary with pipeline trigger response.
"""
payload = {"pipeline_type": pipeline_type, "tickers": tickers}
if config:
payload["config"] = config
response = await self._client.post("/api/pipeline/trigger", json=payload)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
"""News service client for news enrichment operations."""
import httpx
class NewsServiceClient:
"""Async client for the News Service API."""
def __init__(self, base_url: str = "http://localhost:8002"):
"""Initialize the client with a base URL.
Args:
base_url: Base URL for the news service API.
"""
self.base_url = base_url.rstrip("/")
self._client: httpx.AsyncClient | None = None
async def __aenter__(self) -> "NewsServiceClient":
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
if self._client:
await self._client.aclose()
async def get_enriched_news(
self,
ticker: str,
start_date: str | None = None,
end_date: str | None = None,
) -> dict:
"""Get enriched news for a ticker.
Args:
ticker: Stock ticker symbol.
start_date: Start date (YYYY-MM-DD).
end_date: End date (YYYY-MM-DD).
Returns:
Dictionary with enriched news data.
"""
params = {"ticker": ticker}
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
response = await self._client.get("/api/enriched-news", params=params)
response.raise_for_status()
return response.json()
async def get_similar_days(
self,
ticker: str,
date: str,
n_similar: int = 5,
) -> dict:
"""Get similar trading days based on price patterns.
Args:
ticker: Stock ticker symbol.
date: Reference date (YYYY-MM-DD).
n_similar: Number of similar days to return.
Returns:
Dictionary with similar day data.
"""
params = {"ticker": ticker, "date": date, "n_similar": n_similar}
response = await self._client.get("/api/similar-days", params=params)
response.raise_for_status()
return response.json()
async def get_story(self, story_id: str) -> dict:
"""Get a specific news story by ID.
Args:
story_id: The story identifier.
Returns:
Dictionary with story data.
"""
response = await self._client.get(f"/api/stories/{story_id}")
response.raise_for_status()
return response.json()
async def post_enrich(self, news_items: list[dict]) -> dict:
"""Enrich news items with additional analysis.
Args:
news_items: List of news items to enrich.
Returns:
Dictionary with enriched news data.
"""
response = await self._client.post("/api/enrich", json=news_items)
response.raise_for_status()
return response.json()
async def get_categories(self) -> dict:
"""Get available news categories.
Returns:
Dictionary with available categories.
"""
response = await self._client.get("/api/categories")
response.raise_for_status()
return response.json()
async def search_news(
self,
query: str,
ticker: str | None = None,
limit: int = 10,
) -> dict:
"""Search news articles.
Args:
query: Search query string.
ticker: Optional ticker to filter by.
limit: Maximum number of results.
Returns:
Dictionary with search results.
"""
params = {"query": query, "limit": limit}
if ticker:
params["ticker"] = ticker
response = await self._client.get("/api/search", params=params)
response.raise_for_status()
return response.json()

View File

@@ -0,0 +1,207 @@
# -*- coding: utf-8 -*-
"""Trading service client for market data operations."""
import httpx
from shared.schema.price import PriceResponse
from shared.schema.financial import FinancialMetricsResponse, LineItemResponse
from shared.schema.market import InsiderTradeResponse, CompanyFactsResponse
from shared.schema.portfolio import Portfolio
class TradingServiceClient:
"""Async client for the Trading Service API."""
def __init__(self, base_url: str = "http://localhost:8001"):
"""Initialize the client with a base URL.
Args:
base_url: Base URL for the trading service API.
"""
self.base_url = base_url.rstrip("/")
self._client: httpx.AsyncClient | None = None
async def __aenter__(self) -> "TradingServiceClient":
self._client = httpx.AsyncClient(base_url=self.base_url, timeout=30.0)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
if self._client:
await self._client.aclose()
async def get_prices(
self,
ticker: str,
start_date: str | None = None,
end_date: str | None = None,
) -> PriceResponse:
"""Get price data for a ticker.
Args:
ticker: Stock ticker symbol.
start_date: Start date (YYYY-MM-DD).
end_date: End date (YYYY-MM-DD).
Returns:
PriceResponse with price data.
"""
params = {"ticker": ticker}
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
response = await self._client.get("/api/prices", params=params)
response.raise_for_status()
return PriceResponse.model_validate(response.json())
async def get_news(
self,
ticker: str,
start_date: str | None = None,
end_date: str | None = None,
) -> dict:
"""Get news for a ticker.
Args:
ticker: Stock ticker symbol.
start_date: Start date (YYYY-MM-DD).
end_date: End date (YYYY-MM-DD).
Returns:
Dictionary with news data.
"""
params = {"ticker": ticker}
if start_date:
params["start_date"] = start_date
if end_date:
params["end_date"] = end_date
response = await self._client.get("/api/news", params=params)
response.raise_for_status()
return response.json()
async def get_financials(
self,
ticker: str,
period: str | None = None,
limit: int | None = None,
) -> FinancialMetricsResponse:
"""Get financial metrics for a ticker.
Args:
ticker: Stock ticker symbol.
period: Reporting period (e.g., "annual", "quarterly").
limit: Maximum number of records to return.
Returns:
FinancialMetricsResponse with financial data.
"""
params = {"ticker": ticker}
if period:
params["period"] = period
if limit:
params["limit"] = limit
response = await self._client.get("/api/financials", params=params)
response.raise_for_status()
return FinancialMetricsResponse.model_validate(response.json())
async def get_insider_trades(
self,
ticker: str,
limit: int | None = None,
) -> InsiderTradeResponse:
"""Get insider trades for a ticker.
Args:
ticker: Stock ticker symbol.
limit: Maximum number of records to return.
Returns:
InsiderTradeResponse with insider trade data.
"""
params = {"ticker": ticker}
if limit:
params["limit"] = limit
response = await self._client.get("/api/insider-trades", params=params)
response.raise_for_status()
return InsiderTradeResponse.model_validate(response.json())
async def get_portfolio(self) -> Portfolio:
"""Get the current portfolio.
Returns:
Portfolio with current positions and cash.
"""
response = await self._client.get("/api/portfolio")
response.raise_for_status()
return Portfolio.model_validate(response.json())
async def post_trades(self, trades: list[dict]) -> dict:
"""Submit trades for execution.
Args:
trades: List of trade orders.
Returns:
Dictionary with trade execution results.
"""
response = await self._client.post("/api/trades", json=trades)
response.raise_for_status()
return response.json()
async def post_settle(self) -> dict:
"""Settle all pending trades.
Returns:
Dictionary with settlement results.
"""
response = await self._client.post("/api/settle")
response.raise_for_status()
return response.json()
async def get_market_status(self) -> dict:
"""Get current market status.
Returns:
Dictionary with market status information.
"""
response = await self._client.get("/api/market/status")
response.raise_for_status()
return response.json()
async def get_company_facts(self, ticker: str) -> CompanyFactsResponse:
"""Get company facts for a ticker.
Args:
ticker: Stock ticker symbol.
Returns:
CompanyFactsResponse with company information.
"""
response = await self._client.get(f"/api/company/{ticker}/facts")
response.raise_for_status()
return CompanyFactsResponse.model_validate(response.json())
async def get_line_items(
self,
ticker: str,
statement_type: str | None = None,
period: str | None = None,
) -> LineItemResponse:
"""Get line items (financial statement data) for a ticker.
Args:
ticker: Stock ticker symbol.
statement_type: Type of statement (income, balance, cash_flow).
period: Reporting period.
Returns:
LineItemResponse with financial statement data.
"""
params = {"ticker": ticker}
if statement_type:
params["statement_type"] = statement_type
if period:
params["period"] = period
response = await self._client.get("/api/line-items", params=params)
response.raise_for_status()
return LineItemResponse.model_validate(response.json())