Initial commit of integrated agent system

This commit is contained in:
cillin
2026-03-30 17:46:44 +08:00
commit 0fa413380c
337 changed files with 75268 additions and 0 deletions

34
backend/apps/__init__.py Normal file
View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""Application surfaces for progressive service extraction."""
from .agent_service import app as agent_app
from .agent_service import create_app as create_agent_app
from .news_service import app as news_app
from .news_service import create_app as create_news_app
from .openclaw_service import app as openclaw_app
from .openclaw_service import create_app as create_openclaw_app
from .runtime_service import app as runtime_app
from .runtime_service import create_app as create_runtime_app
from .trading_service import app as trading_app
from .trading_service import create_app as create_trading_app
from .cors import add_cors_middleware, get_cors_origins
app = agent_app
create_app = create_agent_app
__all__ = [
"app",
"create_app",
"agent_app",
"create_agent_app",
"news_app",
"create_news_app",
"openclaw_app",
"create_openclaw_app",
"runtime_app",
"create_runtime_app",
"trading_app",
"create_trading_app",
"add_cors_middleware",
"get_cors_origins",
]

View File

@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
"""Agent control-plane FastAPI surface."""
from __future__ import annotations
from contextlib import asynccontextmanager
from pathlib import Path
from typing import AsyncGenerator
from fastapi import FastAPI
from backend.apps.cors import add_cors_middleware
from backend.api import agents_router, guard_router, workspaces_router
from backend.agents import AgentFactory, WorkspaceManager, get_registry
# Global instances (initialized on startup)
agent_factory: AgentFactory | None = None
workspace_manager: WorkspaceManager | None = None
def create_app(project_root: Path | None = None) -> FastAPI:
"""Create the agent control-plane app."""
resolved_project_root = project_root or Path(__file__).resolve().parents[2]
@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
"""Initialize workspace and registry state for the control plane."""
global agent_factory, workspace_manager
workspace_manager = WorkspaceManager(project_root=resolved_project_root)
agent_factory = AgentFactory(project_root=resolved_project_root)
agent_factory.workspaces_root.mkdir(parents=True, exist_ok=True)
registry = get_registry()
print("✓ 大时代 API started")
print(f" - Workspaces root: {agent_factory.workspaces_root}")
print(f" - Registered agents: {registry.get_agent_count()}")
yield
print("✓ 大时代 API shutting down")
app = FastAPI(
title="大时代 Agent Service",
description="REST API for the 大时代 multi-agent control plane",
version="0.1.0",
lifespan=lifespan,
)
add_cors_middleware(app)
@app.get("/health")
async def health_check() -> dict[str, object]:
"""Health check endpoint."""
registry = get_registry()
return {
"status": "healthy",
"version": "0.1.0",
"agents_registered": registry.get_agent_count(),
"workspaces_available": (
len(workspace_manager.list_workspaces())
if workspace_manager
else 0
),
}
@app.get("/api/status")
async def api_status() -> dict[str, object]:
"""Get API status and registry information."""
registry = get_registry()
return {
"status": "operational",
"registry": registry.get_stats(),
}
app.include_router(workspaces_router)
app.include_router(agents_router)
app.include_router(guard_router)
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

30
backend/apps/cors.py Normal file
View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
"""Shared CORS configuration for all microservice apps."""
import os
from typing import Sequence
from fastapi.middleware.cors import CORSMiddleware
def get_cors_origins() -> Sequence[str]:
"""Get allowed CORS origins from environment variable.
Defaults to ["*"] for backward compatibility.
Set CORS_ALLOWED_ORIGINS env var (comma-separated) in production.
"""
origins = os.getenv("CORS_ALLOWED_ORIGINS", "").strip()
if not origins:
return ["*"]
return [o.strip() for o in origins.split(",") if o.strip()]
def add_cors_middleware(app: "FastAPI") -> None:
"""Add CORS middleware to app with environment-configured origins."""
app.add_middleware(
CORSMiddleware,
allow_origins=get_cors_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

View File

@@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""
Production frontend service.
Serves the built frontend static files on port 80 (configurable via
FRONTEND_PORT) and proxies API / WebSocket requests to backend services.
"""
import asyncio
import logging
import os
from pathlib import Path
import httpx
import websockets
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import FileResponse, Response
logger = logging.getLogger(__name__)
FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
AGENT_SERVICE_URL = os.getenv("AGENT_SERVICE_URL", "http://localhost:8000")
RUNTIME_SERVICE_URL = os.getenv("RUNTIME_SERVICE_URL", "http://localhost:8003")
GATEWAY_WS_URL = os.getenv("GATEWAY_WS_URL", "")
app = FastAPI(title="EvoTraders Frontend")
async def _resolve_gateway_ws_url() -> str:
"""Resolve the Gateway WebSocket URL dynamically from runtime API."""
if GATEWAY_WS_URL:
return GATEWAY_WS_URL
try:
async with httpx.AsyncClient(timeout=5) as client:
resp = await client.get(f"{RUNTIME_SERVICE_URL}/api/runtime/gateway/port")
data = resp.json()
if data.get("is_running") and data.get("port"):
url = f"ws://localhost:{data['port']}"
logger.info(f"[Frontend] Resolved gateway URL: {url}")
return url
except Exception as e:
logger.warning(f"[Frontend] Failed to resolve gateway port: {e}")
fallback = f"ws://localhost:{os.getenv('GATEWAY_PORT', '8765')}"
logger.info(f"[Frontend] Using fallback gateway URL: {fallback}")
return fallback
# ── API reverse proxy ────────────────────────────────────────────────
@app.api_route(
"/api/{path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
)
async def proxy_api(request: Request, path: str):
"""Forward /api/* requests to the appropriate backend service."""
target = RUNTIME_SERVICE_URL if path.startswith("runtime/") else AGENT_SERVICE_URL
body = await request.body()
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.request(
method=request.method,
url=f"{target}/api/{path}",
content=body,
headers={
k: v
for k, v in request.headers.items()
if k.lower() not in ("host", "transfer-encoding")
},
)
return Response(
content=resp.content,
status_code=resp.status_code,
headers=dict(resp.headers),
)
# ── WebSocket proxy ──────────────────────────────────────────────────
@app.websocket("/ws")
async def proxy_ws(ws: WebSocket):
"""Proxy WebSocket connections to the Gateway (port resolved dynamically)."""
gateway_url = await _resolve_gateway_ws_url()
await ws.accept()
upstream = None
try:
upstream = await websockets.asyncio.client.connect(gateway_url)
async def client_to_upstream():
try:
while True:
data = await ws.receive_text()
await upstream.send(data)
except WebSocketDisconnect:
pass
async def upstream_to_client():
try:
async for msg in upstream:
if isinstance(msg, str):
await ws.send_text(msg)
else:
await ws.send_bytes(msg)
except websockets.exceptions.ConnectionClosed:
pass
await asyncio.gather(client_to_upstream(), upstream_to_client())
except Exception as e:
logger.warning(f"[Frontend] WebSocket proxy error: {e}")
finally:
if upstream:
await upstream.close()
# ── Static files (SPA fallback) ─────────────────────────────────────
if FRONTEND_DIST.is_dir():
@app.get("/{full_path:path}")
async def serve_spa(full_path: str):
"""Serve static files; fall back to index.html for SPA routing."""
file_path = FRONTEND_DIST / full_path
if full_path and file_path.is_file():
return FileResponse(file_path)
return FileResponse(FRONTEND_DIST / "index.html")

View File

@@ -0,0 +1,154 @@
# -*- coding: utf-8 -*-
"""News and explain FastAPI surface."""
from __future__ import annotations
from typing import Any
from fastapi import Depends, FastAPI, Query
from backend.apps.cors import add_cors_middleware
from backend.data.market_store import MarketStore
from backend.domains import news as news_domain
def get_market_store() -> MarketStore:
"""Get the MarketStore singleton dependency."""
return MarketStore.get_instance()
def create_app() -> FastAPI:
"""Create the news/explain service app."""
app = FastAPI(
title="大时代 News Service",
description="Read-only news enrichment and explain service surface extracted from the monolith",
version="0.1.0",
)
add_cors_middleware(app)
@app.get("/health")
async def health_check() -> dict[str, str]:
return {"status": "healthy", "service": "news-service"}
@app.get("/api/enriched-news")
async def api_get_enriched_news(
ticker: str = Query(..., min_length=1),
start_date: str | None = Query(None),
end_date: str | None = Query(None),
limit: int = Query(100, ge=1, le=1000),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_enriched_news(
store,
ticker=ticker,
start_date=start_date,
end_date=end_date,
limit=limit,
refresh_if_stale=False,
)
@app.get("/api/news-for-date")
async def api_get_news_for_date(
ticker: str = Query(..., min_length=1),
date: str = Query(...),
limit: int = Query(20, ge=1, le=100),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_news_for_date(
store,
ticker=ticker,
date=date,
limit=limit,
refresh_if_stale=False,
)
@app.get("/api/news-timeline")
async def api_get_news_timeline(
ticker: str = Query(..., min_length=1),
start_date: str = Query(...),
end_date: str = Query(...),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_news_timeline(
store,
ticker=ticker,
start_date=start_date,
end_date=end_date,
refresh_if_stale=False,
)
@app.get("/api/categories")
async def api_get_categories(
ticker: str = Query(..., min_length=1),
start_date: str | None = Query(None),
end_date: str | None = Query(None),
limit: int = Query(200, ge=1, le=1000),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_news_categories(
store,
ticker=ticker,
start_date=start_date,
end_date=end_date,
limit=limit,
refresh_if_stale=False,
)
@app.get("/api/similar-days")
async def api_get_similar_days(
ticker: str = Query(..., min_length=1),
date: str = Query(...),
n_similar: int = Query(5, ge=1, le=20),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_similar_days_payload(
store,
ticker=ticker,
date=date,
n_similar=n_similar,
refresh_if_stale=False,
)
@app.get("/api/stories/{ticker}")
async def api_get_story(
ticker: str,
as_of_date: str = Query(...),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_story_payload(
store,
ticker=ticker,
as_of_date=as_of_date,
refresh_if_stale=False,
)
@app.get("/api/range-explain")
async def api_get_range_explain(
ticker: str = Query(..., min_length=1),
start_date: str = Query(...),
end_date: str = Query(...),
article_ids: list[str] = Query(default=[]),
limit: int = Query(100, ge=1, le=500),
store: MarketStore = Depends(get_market_store),
) -> dict[str, Any]:
return news_domain.get_range_explain_payload(
store,
ticker=ticker,
start_date=start_date,
end_date=end_date,
article_ids=article_ids,
limit=limit,
refresh_if_stale=False,
)
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8002)

View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""Read-only OpenClaw CLI FastAPI surface."""
from __future__ import annotations
from fastapi import Depends, FastAPI
from backend.api import openclaw_router
from backend.apps.cors import add_cors_middleware
from backend.api.openclaw import get_openclaw_cli_service
def create_app() -> FastAPI:
"""Create the OpenClaw service app."""
app = FastAPI(
title="大时代 OpenClaw Service",
description="Read-only OpenClaw CLI integration service surface",
version="0.1.0",
)
add_cors_middleware(app)
@app.get("/health")
async def health_check(
service=Depends(get_openclaw_cli_service),
) -> dict[str, object]:
return service.health()
@app.get("/api/status")
async def api_status(
service=Depends(get_openclaw_cli_service),
) -> dict[str, object]:
return {
"status": "operational",
"service": "openclaw-service",
"openclaw": service.health(),
}
app.include_router(openclaw_router)
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8004)

View File

@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
"""Dedicated runtime service FastAPI surface."""
from __future__ import annotations
from fastapi import FastAPI
from backend.api import runtime_router
from backend.api.runtime import get_runtime_state
from backend.apps.cors import add_cors_middleware
def create_app() -> FastAPI:
"""Create the runtime service app."""
app = FastAPI(
title="大时代 Runtime Service",
description="Runtime lifecycle and gateway service surface extracted from the monolith",
version="0.1.0",
)
add_cors_middleware(app)
@app.get("/health")
async def health_check() -> dict[str, object]:
"""Health check for the runtime service."""
runtime_state = get_runtime_state()
process = runtime_state.gateway_process
is_running = process is not None and process.poll() is None
return {
"status": "healthy",
"service": "runtime-service",
"gateway_running": is_running,
"gateway_port": runtime_state.gateway_port,
}
@app.get("/api/status")
async def api_status() -> dict[str, object]:
"""Service-level status payload for runtime orchestration."""
runtime_state = get_runtime_state()
process = runtime_state.gateway_process
is_running = process is not None and process.poll() is None
return {
"status": "operational",
"service": "runtime-service",
"runtime": {
"gateway_running": is_running,
"gateway_port": runtime_state.gateway_port,
"has_runtime_manager": runtime_state.runtime_manager is not None,
},
}
app.include_router(runtime_router)
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8003)

View File

@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
"""Trading data FastAPI surface."""
from __future__ import annotations
from typing import Any
from fastapi import FastAPI, Query
from backend.apps.cors import add_cors_middleware
from backend.domains import trading as trading_domain
from shared.schema import (
CompanyNewsResponse,
FinancialMetricsResponse,
InsiderTradeResponse,
LineItemResponse,
PriceResponse,
)
def create_app() -> FastAPI:
"""Create the trading data service app."""
app = FastAPI(
title="大时代 Trading Service",
description="Read-only trading data service surface extracted from the monolith",
version="0.1.0",
)
add_cors_middleware(app)
@app.get("/health")
async def health_check() -> dict[str, str]:
"""Health check endpoint."""
return {"status": "healthy", "service": "trading-service"}
@app.get("/api/prices", response_model=PriceResponse)
async def api_get_prices(
ticker: str = Query(..., min_length=1),
start_date: str = Query(...),
end_date: str = Query(...),
) -> PriceResponse:
payload = trading_domain.get_prices_payload(
ticker=ticker,
start_date=start_date,
end_date=end_date,
)
return PriceResponse(ticker=payload["ticker"], prices=payload["prices"])
@app.get("/api/financials", response_model=FinancialMetricsResponse)
async def api_get_financials(
ticker: str = Query(..., min_length=1),
end_date: str = Query(...),
period: str = Query("ttm"),
limit: int = Query(10, ge=1, le=100),
) -> FinancialMetricsResponse:
payload = trading_domain.get_financials_payload(
ticker=ticker,
end_date=end_date,
period=period,
limit=limit,
)
return FinancialMetricsResponse(financial_metrics=payload["financial_metrics"])
@app.get("/api/news", response_model=CompanyNewsResponse)
async def api_get_news(
ticker: str = Query(..., min_length=1),
end_date: str = Query(...),
start_date: str | None = Query(None),
limit: int = Query(1000, ge=1, le=5000),
) -> CompanyNewsResponse:
payload = trading_domain.get_news_payload(
ticker=ticker,
end_date=end_date,
start_date=start_date,
limit=limit,
)
return CompanyNewsResponse(news=payload["news"])
@app.get("/api/insider-trades", response_model=InsiderTradeResponse)
async def api_get_insider_trades(
ticker: str = Query(..., min_length=1),
end_date: str = Query(...),
start_date: str | None = Query(None),
limit: int = Query(1000, ge=1, le=5000),
) -> InsiderTradeResponse:
payload = trading_domain.get_insider_trades_payload(
ticker=ticker,
end_date=end_date,
start_date=start_date,
limit=limit,
)
return InsiderTradeResponse(insider_trades=payload["insider_trades"])
@app.get("/api/market/status")
async def api_get_market_status() -> dict[str, Any]:
"""Return current market status using the existing market service logic."""
return trading_domain.get_market_status_payload()
@app.get("/api/market-cap")
async def api_get_market_cap(
ticker: str = Query(..., min_length=1),
end_date: str = Query(...),
) -> dict[str, Any]:
"""Return market cap for one ticker/date."""
return trading_domain.get_market_cap_payload(
ticker=ticker,
end_date=end_date,
)
@app.get("/api/line-items", response_model=LineItemResponse)
async def api_get_line_items(
ticker: str = Query(..., min_length=1),
line_items: list[str] = Query(...),
end_date: str = Query(...),
period: str = Query("ttm"),
limit: int = Query(10, ge=1, le=100),
) -> LineItemResponse:
payload = trading_domain.get_line_items_payload(
ticker=ticker,
line_items=line_items,
end_date=end_date,
period=period,
limit=limit,
)
return LineItemResponse(search_results=payload["search_results"])
return app
app = create_app()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)