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:
8
services/ai/Dockerfile
Normal file
8
services/ai/Dockerfile
Normal 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
45
services/ai/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# AI 网关服务
|
||||
|
||||
> 版本:0.1(P5 骨架)
|
||||
> 端口:3008
|
||||
|
||||
## 职责
|
||||
|
||||
AI 网关限界上下文(Python 实现),统一封装 LLM 调用(多模型路由、重试、限流、成本控制)。
|
||||
提供辅助出题、表达优化、分层提问等能力。通过 gRPC 查询 content 题库与 data-ana 学情数据。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.12 + FastAPI 0.115
|
||||
- Pydantic 2 + pydantic-settings
|
||||
- OpenTelemetry(LLM 调用链追踪)
|
||||
- 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 | 日志级别 |
|
||||
23
services/ai/pyproject.toml
Normal file
23
services/ai/pyproject.toml
Normal 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"]
|
||||
0
services/ai/src/ai/__init__.py
Normal file
0
services/ai/src/ai/__init__.py
Normal file
18
services/ai/src/ai/config.py
Normal file
18
services/ai/src/ai/config.py
Normal 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
111
services/ai/src/ai/main.py
Normal 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"},
|
||||
}
|
||||
Reference in New Issue
Block a user