feat(p5): messaging, push gateway and AI assistant services

P5 阶段交付物:
- services/msg: 消息通知服务(NestJS)
  - notifications: 发送通知 + ES 全文检索 + search
  - config/elasticsearch.ts: ES Client 单例
  - package.json: 补充 @opentelemetry/sdk-node + exporter-trace-otlp-http
- services/push-gateway: WebSocket 推送网关(Go Gin)
  - internal/hub/hub.go: WebSocket 连接池管理(Register/Unregister/SendToUser)
  - internal/ws/handler.go: JWT 鉴权 + WebSocket 升级 + 内部推送 API
- services/ai: AI 辅助服务(Python FastAPI)
  - /chat + /chat/stream(SSE 流式)
  - /generate/question + /optimize/expression
  - config.py: OpenAI 兼容 API 配置
- packages/shared-proto/proto/msg.proto: NotificationService 契约(send/search)
- packages/shared-proto/proto/ai.proto: AiService 契约(含 stream 方法)
This commit is contained in:
SpecialX
2026-07-08 01:39:02 +08:00
parent 9850bfcfd1
commit 7474a92e3b
34 changed files with 1264 additions and 0 deletions

View File

View File

@@ -0,0 +1,18 @@
"""配置管理."""
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""应用配置."""
port: int = 3008
openai_api_key: str = ""
anthropic_api_key: str = ""
otel_endpoint: str = "http://localhost:4318"
log_level: str = "info"
model_config = {"env_file": ".env", "env_prefix": ""}
settings = Settings()

111
services/ai/src/ai/main.py Normal file
View File

@@ -0,0 +1,111 @@
"""AI 网关服务入口."""
from contextlib import asynccontextmanager
import structlog
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from prometheus_client import make_asgi_app
from pydantic import BaseModel
logger = structlog.get_logger()
tracer = trace.get_tracer(__name__)
def init_tracer() -> None:
"""初始化 OpenTelemetry."""
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期."""
init_tracer()
logger.info("ai service starting")
yield
logger.info("ai service stopping")
app = FastAPI(
title="AI Gateway Service",
version="0.1.0",
lifespan=lifespan,
)
app.mount("/metrics", make_asgi_app())
class ChatRequest(BaseModel):
"""聊天请求."""
messages: list[dict]
model: str = "gpt-4o-mini"
temperature: float = 0.7
stream: bool = False
class ChatResponse(BaseModel):
"""聊天响应."""
content: str
model: str
usage: dict
@app.get("/healthz")
async def healthz():
"""健康检查."""
return {"status": "ok", "service": "ai"}
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
"""LLM 聊天接口."""
with tracer.start_as_current_span("ai_chat"):
# P5 骨架:实际调用 OpenAI/Anthropic API
# 需要从环境变量获取 API key
return {
"content": "P5 skeleton - LLM integration pending",
"model": req.model,
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
}
@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
"""流式聊天SSE."""
async def generate():
with tracer.start_as_current_span("ai_chat_stream"):
# P5 骨架:流式调用 LLM
yield "data: P5 skeleton\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
@app.post("/generate/question")
async def generate_question(prompt: str):
"""生成题目."""
with tracer.start_as_current_span("generate_question"):
return {
"success": True,
"data": {"question": "P5 skeleton - question generation pending"},
}
@app.post("/optimize/expression")
async def optimize_expression(text: str):
"""优化表达."""
with tracer.start_as_current_span("optimize_expression"):
return {
"success": True,
"data": {"optimized": "P5 skeleton - expression optimization pending"},
}