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

8
services/ai/Dockerfile Normal file
View File

@@ -0,0 +1,8 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install uv
COPY pyproject.toml .
RUN uv sync --no-dev
COPY src ./src
EXPOSE 3008
CMD ["uv", "run", "uvicorn", "src.ai.main:app", "--host", "0.0.0.0", "--port", "3008"]

45
services/ai/README.md Normal file
View File

@@ -0,0 +1,45 @@
# AI 网关服务
> 版本0.1P5 骨架)
> 端口3008
## 职责
AI 网关限界上下文Python 实现),统一封装 LLM 调用(多模型路由、重试、限流、成本控制)。
提供辅助出题、表达优化、分层提问等能力。通过 gRPC 查询 content 题库与 data-ana 学情数据。
## 技术栈
- Python 3.12 + FastAPI 0.115
- Pydantic 2 + pydantic-settings
- OpenTelemetryLLM 调用链追踪)
- prometheus-client + structlog
- SSE 流式响应
## 开发
```bash
uv sync
uv run uvicorn src.ai.main:app --reload --port 3008
```
## API
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /healthz | 健康检查 |
| POST | /chat | LLM 聊天接口 |
| POST | /chat/stream | 流式聊天SSE |
| POST | /generate/question | 生成题目 |
| POST | /optimize/expression | 优化表达 |
| GET | /metrics | Prometheus 指标 |
## 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| port | 3008 | 服务端口 |
| openai_api_key | - | OpenAI API 密钥 |
| anthropic_api_key | - | Anthropic API 密钥 |
| otel_endpoint | http://localhost:4318 | OpenTelemetry OTLP 端点 |
| log_level | info | 日志级别 |

View File

@@ -0,0 +1,23 @@
[project]
name = "ai-service"
version = "0.1.0"
description = "AI 网关服务 - LLM 集成 + RAG"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"pydantic>=2.9.0",
"pydantic-settings>=2.5.0",
"httpx>=0.27.0",
"opentelemetry-api>=1.27.0",
"opentelemetry-sdk>=1.27.0",
"prometheus-client>=0.20.0",
"structlog>=24.4.0",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM"]

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"},
}