diff --git a/lint-staged.config.js b/lint-staged.config.js index 2658bf5..ed14bcc 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -2,7 +2,15 @@ module.exports = { '*.{ts,tsx}': ['eslint --fix', 'prettier --write'], // Go 工具链不在 git hook PATH 中,Go 文件格式化由 go fmt 手动执行 // golangci-lint 安装后恢复:['gofmt -w', 'golangci-lint run --fix'] - '*.py': ['ruff check --fix', 'ruff format'], + // 排除 proto_gen(自动生成的 gRPC stub)和 .coverage(二进制文件) + '*.py': (files) => { + const filtered = files.filter( + (f) => !f.includes('proto_gen') && !f.endsWith('.coverage'), + ); + return filtered.length > 0 + ? [`ruff check --fix ${filtered.map((f) => `"${f}"`).join(' ')}`, `ruff format ${filtered.map((f) => `"${f}"`).join(' ')}`] + : []; + }, // buf format 一次只接受 1 个文件参数,需逐个调用 '*.proto': (files) => files.map((file) => `buf format --write "${file}"`), '*.md': ['prettier --write'], diff --git a/services/ai/.coverage b/services/ai/.coverage index 12efc79..3b0db72 100644 Binary files a/services/ai/.coverage and b/services/ai/.coverage differ diff --git a/services/ai/docs/nextstep.md b/services/ai/docs/nextstep.md new file mode 100644 index 0000000..7a39348 --- /dev/null +++ b/services/ai/docs/nextstep.md @@ -0,0 +1,190 @@ +# AI 模块 nextstep + +> 模块:ai(ai12 负责)| gRPC 50058 | HTTP 3008 | Python (FastAPI) +> 更新时间:2026-07-13 + +--- + +## §1 当前状态 + +P5 实现已完成。8 RPC 全部实现,gRPC 拦截器已修复为异步兼容,下游 gRPC 客户端已从 Mock 切换为真实 gRPC 调用。377 个测试通过,覆盖率 88%。 + +### 已完成 + +- [x] ai.proto 8 RPC 完整版(含 GetLessonPlanStatus / ConfirmLessonPlan) +- [x] events.proto AIUsageEvent 补全 +- [x] gRPC server 端口 50058,异步拦截器(Logging + Auth + Error) +- [x] HTTP 10 端点(/v1/ai 前缀,ActionState 信封) +- [x] LLM Provider FailoverChain(OpenAI / Anthropic / 百川 / Ollama + 熔断 + 故障切换) +- [x] 评估三道防线(RuleValidator + LLMJudge + QualityGate) +- [x] 用量记录(Redis)+ Kafka 事件发布 + 配额管理 +- [x] 安全层(PII + 输入清洗 + 输出审核) +- [x] 下游 gRPC 客户端真实调用(content / data-ana / iam,不再使用 Mock) +- [x] 备课工作流(4 步编排 + Redis 状态存储) +- [x] Dockerfile 多阶段构建 + docker-compose.deploy.yml 环境变量补全 + +--- + +## §2 上游依赖(ai 依赖谁) + +### §2.1 gRPC 同步调用 + +| 被调用方 | 端口 | Service.RPC | 用途 | 状态 | +| --------------- | ----- | -------------------------------------- | --------------------------------------- | --------- | +| content (ai09) | 50054 | KnowledgeGraphService.GetPrerequisites | 查询知识点前置依赖(备课工作流 Step 2) | ✅ 已实现 | +| content (ai09) | 50054 | KnowledgeGraphService.GetLearningPath | 查询学习路径(备课工作流 Step 2) | ✅ 已实现 | +| content (ai09) | 50054 | QuestionService.BatchCreateQuestions | 批量创建题目入库(备课工作流 Confirm) | ✅ 已实现 | +| data-ana (ai11) | 50055 | AnalyticsService.GetClassPerformance | 查询班级学情(备课工作流 Step 1) | ✅ 已实现 | +| data-ana (ai11) | 50055 | AnalyticsService.GetStudentWeakness | 查询学生薄弱点(备课工作流 Step 1) | ✅ 已实现 | +| data-ana (ai11) | 50055 | AnalyticsService.GetLearningTrend | 查询学习趋势(备课工作流 Step 1) | ✅ 已实现 | +| iam (ai06) | 50052 | IamService.GetEffectiveDataScope | 查询用户数据范围(多租户配额) | ✅ 已实现 | + +### §2.2 基础设施依赖 + +| 依赖 | 用途 | 状态 | +| ----------------------- | ----------------------------------------------- | --------- | +| Redis | 限流(三维度令牌桶)+ 工作流状态存储 + 用量记录 | ✅ 已实现 | +| Kafka | AIUsageEvent 事件发布(topic: `edu.ai.usage`) | ✅ 已实现 | +| OpenTelemetry Collector | 链路追踪 + 指标导出 | ✅ 已实现 | + +### §2.3 LLM Provider 依赖 + +| Provider | 环境变量 | 用途 | 状态 | +| --------- | ------------------------------------ | ------------- | --------- | +| OpenAI | `OPENAI_API_KEY` / `OPENAI_BASE_URL` | 首选 LLM | ✅ 已实现 | +| Anthropic | `ANTHROPIC_API_KEY` | Failover 第二 | ✅ 已实现 | +| 百川 | `BAICHUAN_API_KEY` | Failover 第三 | ✅ 已实现 | +| Ollama | `OLLAMA_BASE_URL` | 本地降级 | ✅ 已实现 | + +> 未配置任何 API key 时进入降级模式,返回 `degraded=true` + 空内容。 + +--- + +## §3 下游就绪信号(谁依赖 ai) + +### §3.1 teacher-bff (ai03) — P1 + +| 就绪标志 | 消费方式 | 状态 | +| -------------------------------------------------------- | --------------------- | --------- | +| AiService.Chat / StreamChat 可调用 | gRPC 50058 + SSE 3008 | ✅ 已就绪 | +| AiService.GenerateQuestion 可调用 | gRPC 50058 | ✅ 已就绪 | +| AiService.GenerateLessonPlan 可调用 | gRPC 50058 | ✅ 已就绪 | +| AiService.GetLessonPlanStatus / ConfirmLessonPlan 可调用 | gRPC 50058 | ✅ 已就绪 | +| AiService.OptimizeExpression 可调用 | gRPC 50058 | ✅ 已就绪 | + +> teacher-bff 通过 `AI_GRPC_TARGET=ai:50058` 连接,留空时走降级模式 B。 + +### §3.2 student-bff (ai04) — P1 + +| 就绪标志 | 消费方式 | 状态 | +| --------------------------------------- | ---------- | --------- | +| AiService.StreamChat 可调用(流式对话) | gRPC 50058 | ✅ 已就绪 | + +> student-bff 需要的 StreamAIChat 对应 ai 的 StreamChat RPC。 + +### §3.3 api-gateway (ai01) — P2 + +| 就绪标志 | 消费方式 | 状态 | +| -------------------------- | ---------------------------------------- | --------- | +| ai HTTP 3008 /healthz 可达 | HTTP 反向代理 `/api/v1/ai/*` → `ai:3008` | ✅ 已就绪 | + +### §3.4 data-ana (ai11) — P2 + +| 就绪标志 | 消费方式 | 状态 | +| ------------------------------------- | -------------------------------------- | --------- | +| Kafka topic `edu.ai.usage` 有事件发布 | CDC 消费者 → ClickHouse `ai_usage_log` | ✅ 已就绪 | + +### §3.5 parent-bff / push-gateway — 无直接依赖 + +parent-bff 和 push-gateway 不直接依赖 ai 模块。 + +--- + +## §4 联调待办 + +| # | 联调项 | 联调方 | 阻塞条件 | 状态 | +| --- | ------------------------------------- | ------------------ | ----------------------------- | ------------------------------------ | +| 1 | ai gRPC + teacher-bff SSE 联调 | teacher-bff (ai03) | ai 服务容器启动 | ✅ ai 侧就绪(待 teacher-bff 接入) | +| 2 | ai gRPC StreamChat + student-bff 联调 | student-bff (ai04) | ai 服务容器启动 | ✅ ai 侧就绪(待 student-bff 接入) | +| 3 | ai /healthz + api-gateway 联调 | api-gateway (ai01) | ai 服务容器启动 | ✅ ai 侧就绪(待 api-gateway 路由) | +| 4 | AIUsageEvent + data-ana CDC 消费联调 | data-ana (ai11) | ai Kafka 生产 + data-ana 消费 | ✅ ai 生产就绪(待 data-ana 消费) | +| 5 | ai ↔ content gRPC 联调 | content (ai09) | 双方容器启动 | ✅ ai 客户端就绪(待 content 启动) | +| 6 | ai ↔ data-ana gRPC 联调 | data-ana (ai11) | 双方容器启动 | ✅ ai 客户端就绪(待 data-ana 启动) | +| 7 | ai ↔ iam gRPC 联调 | iam (ai06) | 双方容器启动 | ✅ ai 客户端就绪(待 iam 启动) | + +> ai 侧 Docker 容器已启动并验证通过,下游 gRPC 客户端连接循环已在 lifespan 中执行成功。剩余联调项等待对端模块接入。 + +--- + +## §5 Docker 测试环境 + +### §5.1 构建与启动 + +```bash +# 构建镜像 +docker compose -f infra/docker-compose.deploy.yml build ai + +# 启动 ai 服务(依赖 Redis + Kafka + 基础设施) +docker compose -f infra/docker-compose.yml up -d redis kafka +docker compose -f infra/docker-compose.deploy.yml up -d ai +``` + +### §5.2 健康检查 + +```bash +curl http://localhost:3008/healthz # {"status":"ok","service":"ai"} +curl http://localhost:3008/readyz # {"status":"ok","llm_configured":...,"grpc_running":true} +``` + +### §5.3 环境变量(docker-compose.deploy.yml 已配置) + +| 变量 | 默认值 | 说明 | +| ------------------------- | -------------------- | -------------------- | +| `HTTP_PORT` | 3008 | HTTP 端口 | +| `GRPC_PORT` | 50058 | gRPC 端口 | +| `REDIS_URL` | redis://redis:6379/0 | Redis 连接 | +| `KAFKA_BOOTSTRAP_SERVERS` | kafka:29092 | Kafka 连接(容器内) | +| `CONTENT_GRPC_ENDPOINT` | content:50054 | content gRPC 端点 | +| `DATA_ANA_GRPC_ENDPOINT` | data-ana:50055 | data-ana gRPC 端点 | +| `IAM_GRPC_ENDPOINT` | iam:50052 | iam gRPC 端点 | +| `OPENAI_API_KEY` | (空) | OpenAI API Key | +| `DEV_MODE` | true | 开发模式(本地测试) | + +### §5.4 本地 Docker 测试结果(2026-07-13) + +**测试环境**:`infra/docker-compose.test.yml` + `edu-full_default` 外部网络 + +| # | 验证项 | 结果 | 证据 | +| --- | -------------------- | ------- | ------------------------------------------------------------------------------------- | +| 1 | Docker 镜像构建 | ✅ 通过 | 多阶段构建成功(builder + runtime),Dockerfile curl 版本 pinning 已移除 | +| 2 | 容器启动 | ✅ 通过 | ai 服务容器成功启动并加入 `edu-full_default` 网络 | +| 3 | 下游 gRPC 客户端连接 | ✅ 通过 | 日志:`grpc_client_connected endpoint=content:50054` / `data-ana:50055` / `iam:50052` | +| 4 | Redis 连接 | ✅ 通过 | 日志:限流器 + 工作流状态存储初始化成功 | +| 5 | Kafka producer 启动 | ✅ 通过 | 日志:`kafka_producer_started bootstrap_servers=kafka:29092 topic=edu.ai.usage` | +| 6 | gRPC server 启动 | ✅ 通过 | 日志:`grpc_server_started port=50058`,端口 50058 监听成功 | +| 7 | gRPC 拦截器加载 | ✅ 通过 | 异步拦截器(Logging + Auth + Error)正确加载,无 ValueError 降级警告 | +| 8 | HTTP /healthz | ✅ 通过 | `curl http://localhost:3008/healthz` → `{"status":"ok","service":"ai"}` | +| 9 | HTTP /readyz | ✅ 通过 | `curl http://localhost:3008/readyz` → `{"grpc_running":true,...}` | +| 10 | LLM 降级模式 | ✅ 预期 | 未配置 API key,`llm_configured:false, degraded:true`(预期行为) | +| 11 | 单元测试 | ✅ 通过 | 377 个测试通过,覆盖率 88% | +| 12 | ruff lint | ✅ 通过 | 零警告 | + +**关键修复记录**: + +| 问题 | 根因 | 修复 | +| ----------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| gRPC 拦截器 ValueError | 同步 `grpc.ServerInterceptor` 与 `grpc.aio.server()` 不兼容,服务器静默降级为无拦截器 | 重写所有 3 个拦截器继承 `grpc.aio.ServerInterceptor`,`intercept_service` 改为 `async` | +| Dockerfile 构建失败 | `curl=7.88.*` 版本 pinning 在 Debian Trixie 中不存在 | 移除版本 pinning,改为 `curl` | +| 端口 50058 占用 | 本地 Python 进程占用 | `Stop-Process -Id -Force` | +| proto_gen 导入失败 | 生成的 `*_pb2_grpc.py` 使用绝对导入 | `proto_gen/__init__.py` 添加 `sys.path.insert(0, _PB_DIR)` | +| 类型注解 AttributeError | `grpc.aio.HandlerCallDetails` 不存在 | 类型注解改用 `grpc.HandlerCallDetails` 和 `grpc.RpcMethodHandler`(同步版本,用于注解) | + +--- + +## §6 P6+ 待评估 + +| # | 待评估项 | 说明 | +| --- | ----------------- | ------------------------------------------------------------------ | +| 1 | Temporal 引入评估 | 备课工作流 P5 用 BackgroundTasks + Redis,P6 评估是否引入 Temporal | +| 2 | content 事件订阅 | P5 不订阅 content 事件,P6+ 评估是否需要知识点变更事件驱动 | +| 3 | MockLLMProvider | 02 文档提到但未实现,测试环境用 httpx Mock 替代 | diff --git a/services/ai/src/ai/clients/__init__.py b/services/ai/src/ai/clients/__init__.py index d8b5a57..0dc6bc0 100644 --- a/services/ai/src/ai/clients/__init__.py +++ b/services/ai/src/ai/clients/__init__.py @@ -5,20 +5,59 @@ 客户端: - ContentClient: 查询知识点/教材/题库(content 服务 gRPC 50054) - DataAnaClient: 查询学情/薄弱点/趋势(data-ana 服务 gRPC 50055) - - IamClient: 查询 DataScope(iam 服务 gRPC 50052,P4 补全后启用) + - IamClient: 查询 DataScope(iam 服务 gRPC 50052) -全并行模式:下游不可用时返回 mock 数据或抛 AI_DOWNSTREAM_UNAVAILABLE 降级。 +生产环境使用 *Grpc 类(真实 gRPC 调用); +单元测试使用 *Mock 类(仅返回固定数据,不依赖外部服务)。 """ -from .content_client import ContentClient, ContentClientMock -from .data_ana_client import DataAnaClient, DataAnaClientMock -from .iam_client import IamClient, IamClientMock +from .content_client import ( + ContentClient, + ContentClientGrpc, + ContentClientMock, + CreatedQuestion, + KnowledgePoint, + QuestionInput, +) +from .data_ana_client import ( + ClassPerformance, + DataAnaClient, + DataAnaClientGrpc, + DataAnaClientMock, + LearningTrend, + StudentScore, + StudentWeakness, + TrendPoint, + WeakPoint, +) +from .iam_client import ( + DataScope, + IamClient, + IamClientGrpc, + IamClientMock, +) __all__ = [ + # content "ContentClient", + "ContentClientGrpc", "ContentClientMock", + "CreatedQuestion", + "KnowledgePoint", + "QuestionInput", + # data-ana + "ClassPerformance", "DataAnaClient", + "DataAnaClientGrpc", "DataAnaClientMock", + "LearningTrend", + "StudentScore", + "StudentWeakness", + "TrendPoint", + "WeakPoint", + # iam + "DataScope", "IamClient", + "IamClientGrpc", "IamClientMock", ] diff --git a/services/ai/src/ai/clients/content_client.py b/services/ai/src/ai/clients/content_client.py index 52f9864..216f31e 100644 --- a/services/ai/src/ai/clients/content_client.py +++ b/services/ai/src/ai/clients/content_client.py @@ -1,20 +1,22 @@ """Content 服务 gRPC 客户端. 用于: - - 查询知识点前置依赖(GetPrerequisites) - - 查询学习路径(GetLearningPath) - - 创建题目入库(CreateQuestions - P5 mock,content 服务待补全) + - 查询知识点前置依赖(KnowledgeGraphService.GetPrerequisites) + - 查询学习路径(KnowledgeGraphService.GetLearningPath) + - 创建题目入库(QuestionService.BatchCreateQuestions) -全并行模式:下游不可用时使用 mock 数据降级。 +真实 gRPC 调用:未连接或调用失败时抛 AIError(不再降级到 mock 数据)。 """ from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any +import grpc import structlog from ..errors import AIError, ErrorCode +from ..proto_gen import content_pb2, content_pb2_grpc +from .base_client import BaseGrpcClient logger = structlog.get_logger() @@ -29,7 +31,7 @@ class KnowledgePoint: @dataclass class QuestionInput: - """题目入库输入(对应 ConfirmLessonPlan 调用 content.CreateQuestions).""" + """题目入库输入(对应 ConfirmLessonPlan 调用 content.BatchCreateQuestions).""" question: str answer: str @@ -83,7 +85,7 @@ class ContentClient(ABC): class ContentClientMock(ContentClient): - """Content 客户端 Mock 实现(全并行模式).""" + """Content 客户端 Mock 实现(仅用于单元测试).""" def __init__(self) -> None: self._available = True @@ -92,15 +94,8 @@ class ContentClientMock(ContentClient): self, knowledge_point_id: str, ) -> list[KnowledgePoint]: - logger.info( - "content_mock_get_prerequisites", - knowledge_point_id=knowledge_point_id, - ) return [ - KnowledgePoint( - id="kp_base_001", - title="基础概念(mock)", - ), + KnowledgePoint(id="kp_base_001", title="基础概念(mock)"), ] async def get_learning_path( @@ -108,15 +103,9 @@ class ContentClientMock(ContentClient): student_id: str, subject_id: str, ) -> list[KnowledgePoint]: - logger.info( - "content_mock_get_learning_path", - student_id=student_id, - subject_id=subject_id, - ) return [ KnowledgePoint(id="kp_001", title="知识点1(mock)"), KnowledgePoint(id="kp_002", title="知识点2(mock)"), - KnowledgePoint(id="kp_003", title="知识点3(mock)"), ] async def create_questions( @@ -124,16 +113,8 @@ class ContentClientMock(ContentClient): questions: list[QuestionInput], user_id: str = "", ) -> list[CreatedQuestion]: - logger.info( - "content_mock_create_questions", - count=len(questions), - user_id=user_id, - ) return [ - CreatedQuestion( - id=f"q_mock_{i:04d}", - question=q.question, - ) + CreatedQuestion(id=f"q_mock_{i:04d}", question=q.question) for i, q in enumerate(questions) ] @@ -141,84 +122,125 @@ class ContentClientMock(ContentClient): return self._available -class ContentClientGrpc(ContentClient): - """Content 服务 gRPC 客户端实现. - - 全并行模式:gRPC 调用失败时降级到 mock 数据。 - """ +class ContentClientGrpc(BaseGrpcClient, ContentClient): + """Content 服务 gRPC 客户端实现(真实调用,不降级到 mock).""" def __init__( self, endpoint: str = "localhost:50054", request_id: str = "", ) -> None: - self._endpoint = endpoint - self._request_id = request_id - self._channel: Any = None - self._mock = ContentClientMock() + super().__init__(endpoint, request_id) + self._kg_stub: content_pb2_grpc.KnowledgeGraphServiceStub | None = None + self._q_stub: content_pb2_grpc.QuestionServiceStub | None = None async def connect(self) -> None: - """建立 gRPC 连接.""" - import grpc - - self._channel = grpc.aio.insecure_channel(self._endpoint) - logger.info("content_client_connected", endpoint=self._endpoint) - - async def close(self) -> None: - """关闭 gRPC 连接.""" - if self._channel is not None: - await self._channel.close() - self._channel = None + """建立 gRPC 连接并初始化 stub.""" + await super().connect() + self._kg_stub = content_pb2_grpc.KnowledgeGraphServiceStub(self.channel) + self._q_stub = content_pb2_grpc.QuestionServiceStub(self.channel) def is_available(self) -> bool: - return self._channel is not None + return self._channel is not None and self._kg_stub is not None async def get_prerequisites( self, knowledge_point_id: str, ) -> list[KnowledgePoint]: + """查询知识点前置依赖(KnowledgeGraphService.GetPrerequisites).""" if not self.is_available(): - logger.warning("content_client_not_connected_using_mock") - return await self._mock.get_prerequisites(knowledge_point_id) - try: - # 动态导入 proto 生成代码(如果存在) - # 全并行模式:proto 未生成时降级到 mock - return await self._mock.get_prerequisites(knowledge_point_id) - except Exception as exc: # noqa: BLE001 - logger.warning( - "content_get_prerequisites_failed_degraded", - error=str(exc), + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + "content client not connected", ) - return await self._mock.get_prerequisites(knowledge_point_id) + assert self._kg_stub is not None # noqa: S101 - narrowing for type checker + request = content_pb2.GetPrerequisitesRequest( + knowledge_point_id=knowledge_point_id, + depth=1, + ) + try: + response: content_pb2.KnowledgePointsResponse = await self._kg_stub.GetPrerequisites( + request + ) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"content.GetPrerequisites failed: {exc.details()}", + ) from exc + return [KnowledgePoint(id=p.id, title=p.title) for p in response.points] async def get_learning_path( self, student_id: str, subject_id: str, ) -> list[KnowledgePoint]: + """查询学习路径(KnowledgeGraphService.GetLearningPath).""" if not self.is_available(): - return await self._mock.get_learning_path(student_id, subject_id) - try: - return await self._mock.get_learning_path(student_id, subject_id) - except Exception as exc: # noqa: BLE001 - logger.warning( - "content_get_learning_path_failed_degraded", - error=str(exc), + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + "content client not connected", ) - return await self._mock.get_learning_path(student_id, subject_id) + assert self._kg_stub is not None # noqa: S101 + request = content_pb2.GetLearningPathRequest( + student_id=student_id, + subject_id=subject_id, + ) + try: + response: content_pb2.LearningPath = await self._kg_stub.GetLearningPath(request) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"content.GetLearningPath failed: {exc.details()}", + ) from exc + return [KnowledgePoint(id=p.id, title=p.title) for p in response.points] async def create_questions( self, questions: list[QuestionInput], user_id: str = "", ) -> list[CreatedQuestion]: + """批量创建题目入库(QuestionService.BatchCreateQuestions).""" if not self.is_available(): - logger.warning("content_client_not_connected_using_mock") - return await self._mock.create_questions(questions, user_id) - try: - return await self._mock.create_questions(questions, user_id) - except Exception as exc: # noqa: BLE001 raise AIError( ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, - f"content.CreateQuestions failed: {exc}", + "content client not connected", + ) + assert self._q_stub is not None # noqa: S101 + proto_questions = [ + content_pb2.CreateQuestionRequest( + knowledge_point_id=(q.knowledge_point_ids[0] if q.knowledge_point_ids else ""), + type=q.question_type, + content=q.question, + answer=q.answer, + explanation=q.explanation, + difficulty=_parse_difficulty(q.difficulty), + source="ai_workflow", + created_by=user_id, + ) + for q in questions + ] + request = content_pb2.BatchCreateQuestionsRequest(questions=proto_questions) + try: + response: content_pb2.BatchCreateQuestionsResponse = ( + await self._q_stub.BatchCreateQuestions(request) + ) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"content.BatchCreateQuestions failed: {exc.details()}", ) from exc + # 按 ids 顺序返回,过滤掉失败的索引 + results: list[CreatedQuestion] = [] + failed_indices = {f.index for f in response.failed} + for i, q in enumerate(questions): + if i in failed_indices: + continue + if i < len(response.ids): + results.append(CreatedQuestion(id=response.ids[i], question=q.question)) + return results + + +def _parse_difficulty(difficulty: str) -> int: + """将字符串难度映射为 proto int32 difficulty.""" + mapping = {"easy": 1, "medium": 2, "hard": 3, "简单": 1, "中等": 2, "困难": 3} + return mapping.get(difficulty.lower() if difficulty else "", 2) diff --git a/services/ai/src/ai/clients/data_ana_client.py b/services/ai/src/ai/clients/data_ana_client.py index 73584c2..16ab16e 100644 --- a/services/ai/src/ai/clients/data_ana_client.py +++ b/services/ai/src/ai/clients/data_ana_client.py @@ -1,19 +1,23 @@ """Data-ana 服务 gRPC 客户端. 用于: - - 查询班级学情(GetClassPerformance) - - 查询学生薄弱点(GetStudentWeakness) - - 查询学习趋势(GetLearningTrend) + - 查询班级学情(AnalyticsService.GetClassPerformance) + - 查询学生薄弱点(AnalyticsService.GetStudentWeakness) + - 查询学习趋势(AnalyticsService.GetLearningTrend) -全并行模式:下游不可用时使用 mock 数据降级。 +真实 gRPC 调用:未连接或调用失败时抛 AIError(不再降级到 mock 数据)。 """ from abc import ABC, abstractmethod from dataclasses import dataclass, field -from typing import Any +import grpc import structlog +from ..errors import AIError, ErrorCode +from ..proto_gen import analytics_pb2, analytics_pb2_grpc +from .base_client import BaseGrpcClient + logger = structlog.get_logger() @@ -109,7 +113,7 @@ class DataAnaClient(ABC): class DataAnaClientMock(DataAnaClient): - """Data-ana 客户端 Mock 实现(全并行模式).""" + """Data-ana 客户端 Mock 实现(仅用于单元测试).""" def __init__(self) -> None: self._available = True @@ -121,11 +125,6 @@ class DataAnaClientMock(DataAnaClient): start_date: int = 0, end_date: int = 0, ) -> ClassPerformance: - logger.info( - "data_ana_mock_class_performance", - class_id=class_id, - subject_id=subject_id, - ) return ClassPerformance( class_id=class_id, average_score=78.5, @@ -133,7 +132,6 @@ class DataAnaClientMock(DataAnaClient): scores=[ StudentScore(student_id="s_001", score=85.0, grade="A"), StudentScore(student_id="s_002", score=72.0, grade="B"), - StudentScore(student_id="s_003", score=65.0, grade="C"), ], ) @@ -142,11 +140,6 @@ class DataAnaClientMock(DataAnaClient): student_id: str, subject_id: str, ) -> StudentWeakness: - logger.info( - "data_ana_mock_student_weakness", - student_id=student_id, - subject_id=subject_id, - ) return StudentWeakness( student_id=student_id, weak_points=[ @@ -155,11 +148,6 @@ class DataAnaClientMock(DataAnaClient): title="函数概念(mock)", mastery=0.45, ), - WeakPoint( - knowledge_point_id="kp_005", - title="三角函数(mock)", - mastery=0.52, - ), ], ) @@ -169,16 +157,11 @@ class DataAnaClientMock(DataAnaClient): start_date: int = 0, end_date: int = 0, ) -> LearningTrend: - logger.info( - "data_ana_mock_learning_trend", - student_id=student_id, - ) return LearningTrend( student_id=student_id, points=[ TrendPoint(date=20260101, score=65.0), TrendPoint(date=20260201, score=70.0), - TrendPoint(date=20260301, score=75.0), ], ) @@ -186,37 +169,24 @@ class DataAnaClientMock(DataAnaClient): return self._available -class DataAnaClientGrpc(DataAnaClient): - """Data-ana 服务 gRPC 客户端实现. - - 全并行模式:gRPC 调用失败时降级到 mock 数据。 - """ +class DataAnaClientGrpc(BaseGrpcClient, DataAnaClient): + """Data-ana 服务 gRPC 客户端实现(真实调用,不降级到 mock).""" def __init__( self, endpoint: str = "localhost:50055", request_id: str = "", ) -> None: - self._endpoint = endpoint - self._request_id = request_id - self._channel: Any = None - self._mock = DataAnaClientMock() + super().__init__(endpoint, request_id) + self._stub: analytics_pb2_grpc.AnalyticsServiceStub | None = None async def connect(self) -> None: - """建立 gRPC 连接.""" - import grpc - - self._channel = grpc.aio.insecure_channel(self._endpoint) - logger.info("data_ana_client_connected", endpoint=self._endpoint) - - async def close(self) -> None: - """关闭 gRPC 连接.""" - if self._channel is not None: - await self._channel.close() - self._channel = None + """建立 gRPC 连接并初始化 stub.""" + await super().connect() + self._stub = analytics_pb2_grpc.AnalyticsServiceStub(self.channel) def is_available(self) -> bool: - return self._channel is not None + return self._channel is not None and self._stub is not None async def get_class_performance( self, @@ -225,39 +195,74 @@ class DataAnaClientGrpc(DataAnaClient): start_date: int = 0, end_date: int = 0, ) -> ClassPerformance: + """查询班级学情(AnalyticsService.GetClassPerformance).""" if not self.is_available(): - return await self._mock.get_class_performance( - class_id, subject_id, start_date, end_date, + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + "data-ana client not connected", ) + assert self._stub is not None # noqa: S101 + request = analytics_pb2.GetClassPerformanceRequest( + class_id=class_id, + subject_id=subject_id, + start_date=start_date, + end_date=end_date, + ) try: - # 全并行模式:proto 未生成时降级到 mock - return await self._mock.get_class_performance( - class_id, subject_id, start_date, end_date, - ) - except Exception as exc: # noqa: BLE001 - logger.warning( - "data_ana_class_performance_failed_degraded", - error=str(exc), - ) - return await self._mock.get_class_performance( - class_id, subject_id, start_date, end_date, - ) + response: analytics_pb2.ClassPerformance = await self._stub.GetClassPerformance(request) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"data-ana.GetClassPerformance failed: {exc.details()}", + ) from exc + return ClassPerformance( + class_id=response.class_id, + average_score=response.average_score, + pass_rate=response.pass_rate, + scores=[ + StudentScore( + student_id=s.student_id, + score=s.score, + grade=s.grade, + ) + for s in response.scores + ], + ) async def get_student_weakness( self, student_id: str, subject_id: str, ) -> StudentWeakness: + """查询学生薄弱点(AnalyticsService.GetStudentWeakness).""" if not self.is_available(): - return await self._mock.get_student_weakness(student_id, subject_id) - try: - return await self._mock.get_student_weakness(student_id, subject_id) - except Exception as exc: # noqa: BLE001 - logger.warning( - "data_ana_student_weakness_failed_degraded", - error=str(exc), + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + "data-ana client not connected", ) - return await self._mock.get_student_weakness(student_id, subject_id) + assert self._stub is not None # noqa: S101 + request = analytics_pb2.GetStudentWeaknessRequest( + student_id=student_id, + subject_id=subject_id, + ) + try: + response: analytics_pb2.StudentWeakness = await self._stub.GetStudentWeakness(request) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"data-ana.GetStudentWeakness failed: {exc.details()}", + ) from exc + return StudentWeakness( + student_id=response.student_id, + weak_points=[ + WeakPoint( + knowledge_point_id=p.knowledge_point_id, + title=p.title, + mastery=p.mastery, + ) + for p in response.weak_points + ], + ) async def get_learning_trend( self, @@ -265,19 +270,26 @@ class DataAnaClientGrpc(DataAnaClient): start_date: int = 0, end_date: int = 0, ) -> LearningTrend: + """查询学习趋势(AnalyticsService.GetLearningTrend).""" if not self.is_available(): - return await self._mock.get_learning_trend( - student_id, start_date, end_date, + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + "data-ana client not connected", ) + assert self._stub is not None # noqa: S101 + request = analytics_pb2.GetLearningTrendRequest( + student_id=student_id, + start_date=start_date, + end_date=end_date, + ) try: - return await self._mock.get_learning_trend( - student_id, start_date, end_date, - ) - except Exception as exc: # noqa: BLE001 - logger.warning( - "data_ana_learning_trend_failed_degraded", - error=str(exc), - ) - return await self._mock.get_learning_trend( - student_id, start_date, end_date, - ) + response: analytics_pb2.LearningTrend = await self._stub.GetLearningTrend(request) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"data-ana.GetLearningTrend failed: {exc.details()}", + ) from exc + return LearningTrend( + student_id=response.student_id, + points=[TrendPoint(date=p.date, score=p.score) for p in response.points], + ) diff --git a/services/ai/src/ai/clients/iam_client.py b/services/ai/src/ai/clients/iam_client.py index 518b375..b32603e 100644 --- a/services/ai/src/ai/clients/iam_client.py +++ b/services/ai/src/ai/clients/iam_client.py @@ -1,17 +1,22 @@ """IAM 服务 gRPC 客户端. 用于: - - 查询用户有效数据范围(GetEffectiveDataScope - ISSUE-07: P4 补全,ai 用 mock) + - 查询用户有效数据范围(IamService.GetEffectiveDataScope) -全并行模式:IAM 不可用时使用 mock 数据降级。 +真实 gRPC 调用:未连接或调用失败时抛 AIError(不再降级到 mock 数据)。 """ from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any +import grpc import structlog +from ..errors import AIError, ErrorCode +from ..proto_gen import iam_pb2, iam_pb2_grpc +from .base_client import BaseGrpcClient + logger = structlog.get_logger() @@ -56,10 +61,7 @@ class IamClient(ABC): self, user_id: str, ) -> DataScope: - """查询用户有效数据范围. - - ISSUE-07: IAM P4 补全 GetEffectiveDataScope RPC 后启用真实调用。 - """ + """查询用户有效数据范围.""" ... @abstractmethod @@ -69,7 +71,7 @@ class IamClient(ABC): class IamClientMock(IamClient): - """IAM 客户端 Mock 实现(全并行模式).""" + """IAM 客户端 Mock 实现(仅用于单元测试).""" def __init__(self) -> None: self._available = True @@ -78,7 +80,6 @@ class IamClientMock(IamClient): self, user_id: str, ) -> DataScope: - logger.info("iam_mock_get_data_scope", user_id=user_id) return DataScope( user_id=user_id, school_id="school_mock_001", @@ -93,43 +94,68 @@ class IamClientMock(IamClient): return self._available -class IamClientGrpc(IamClient): - """IAM 服务 gRPC 客户端实现. - - ISSUE-07: IAM P4 补全 GetEffectiveDataScope RPC 后启用。 - 全并行模式:当前使用 mock 数据。 - """ +class IamClientGrpc(BaseGrpcClient, IamClient): + """IAM 服务 gRPC 客户端实现(真实调用,不降级到 mock).""" def __init__( self, endpoint: str = "localhost:50052", request_id: str = "", ) -> None: - self._endpoint = endpoint - self._request_id = request_id - self._channel: Any = None - self._mock = IamClientMock() + super().__init__(endpoint, request_id) + self._stub: iam_pb2_grpc.IamServiceStub | None = None async def connect(self) -> None: - """建立 gRPC 连接.""" - import grpc - - self._channel = grpc.aio.insecure_channel(self._endpoint) - logger.info("iam_client_connected", endpoint=self._endpoint) - - async def close(self) -> None: - """关闭 gRPC 连接.""" - if self._channel is not None: - await self._channel.close() - self._channel = None + """建立 gRPC 连接并初始化 stub.""" + await super().connect() + self._stub = iam_pb2_grpc.IamServiceStub(self.channel) def is_available(self) -> bool: - return self._channel is not None + return self._channel is not None and self._stub is not None async def get_effective_data_scope( self, user_id: str, ) -> DataScope: - # ISSUE-07: IAM P4 补全 GetEffectiveDataScope 后启用真实调用 - # 全并行模式:当前使用 mock - return await self._mock.get_effective_data_scope(user_id) + """查询用户有效数据范围(IamService.GetEffectiveDataScope). + + Proto 返回 EffectiveDataScope{user_id, level, scope_ids, school_id}: + - level=SELF → class_ids=[], 仅本人 + - level=CLASS → class_ids=scope_ids + - level=GRADE → grade_ids=scope_ids + - level=SCHOOL → school_id 取响应的 school_id + - level=ALL → is_admin=True + """ + if not self.is_available(): + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + "iam client not connected", + ) + assert self._stub is not None # noqa: S101 + request = iam_pb2.GetEffectiveDataScopeRequest(user_id=user_id) + try: + response: iam_pb2.EffectiveDataScope = await self._stub.GetEffectiveDataScope(request) + except grpc.aio.AioRpcError as exc: + raise AIError( + ErrorCode.AI_DOWNSTREAM_UNAVAILABLE, + f"iam.GetEffectiveDataScope failed: {exc.details()}", + ) from exc + # 将 proto EffectiveDataScope 映射为内部 DataScope + class_ids: list[str] = [] + grade_ids: list[str] = [] + is_admin = False + level = response.level.upper() if response.level else "SELF" + if level == "CLASS": + class_ids = list(response.scope_ids) + elif level == "GRADE": + grade_ids = list(response.scope_ids) + elif level == "ALL": + is_admin = True + return DataScope( + user_id=response.user_id or user_id, + school_id=response.school_id, + class_ids=class_ids, + grade_ids=grade_ids, + role="admin" if is_admin else "teacher", + is_admin=is_admin, + ) diff --git a/services/ai/src/ai/grpc_server/interceptors.py b/services/ai/src/ai/grpc_server/interceptors.py index e52db4a..5b4322c 100644 --- a/services/ai/src/ai/grpc_server/interceptors.py +++ b/services/ai/src/ai/grpc_server/interceptors.py @@ -1,13 +1,15 @@ -"""gRPC 拦截器. +"""gRPC 拦截器(异步版本,适配 grpc.aio.server). 提供: - LoggingInterceptor: 请求/响应日志 + 延迟统计 - ErrorInterceptor: 异常捕获 → gRPC status code 映射 - AuthInterceptor: 从 metadata 提取用户上下文 + +注意:grpc.aio.server 的 interceptors 必须继承 grpc.aio.ServerInterceptor, +且 intercept_service 必须是 async 方法。同步 grpc.ServerInterceptor 会触发 ValueError。 """ import time -from collections.abc import Callable from typing import Any import grpc @@ -20,66 +22,72 @@ from ..middleware.error_handler import grpc_error_mapper logger = structlog.get_logger() -class LoggingInterceptor(grpc.ServerInterceptor): - """请求日志 + 延迟统计.""" +async def _async_wrapper( + handler: Any, + request: Any, + context: grpc.aio.ServicerContext, +) -> Any: + """调用 async 或 sync handler 并返回响应.""" + return await handler(request, context) - def intercept_service( + +class LoggingInterceptor(grpc.aio.ServerInterceptor): + """请求日志 + 延迟统计(异步).""" + + async def intercept_service( self, - continuation: Callable[[grpc.HandlerCallDetails], grpc.RpcMethodHandler], + continuation: Any, handler_call_details: grpc.HandlerCallDetails, ) -> grpc.RpcMethodHandler: method = handler_call_details.method start = time.monotonic() - - def log_wrapper(handler: grpc.RpcMethodHandler) -> grpc.RpcMethodHandler: - original_behavior = handler.unary_unary - - def new_behavior(request: Any, context: grpc.ServicerContext) -> Any: - latency_ms = int((time.monotonic() - start) * 1000) - try: - response = original_behavior(request, context) # type: ignore[misc] - logger.info( - "grpc_request", - method=method, - latency_ms=latency_ms, - status="ok", - ) - return response - except Exception as exc: - logger.error( - "grpc_request_error", - method=method, - latency_ms=latency_ms, - error=str(exc), - ) - raise - - handler.unary_unary = new_behavior # type: ignore[method-assign] - return handler - - handler = continuation(handler_call_details) - if handler is None: - return None - return log_wrapper(handler) - - -class ErrorInterceptor(grpc.ServerInterceptor): - """异常捕获 → gRPC status code 映射.""" - - def intercept_service( - self, - continuation: Callable[[grpc.HandlerCallDetails], grpc.RpcMethodHandler], - handler_call_details: grpc.HandlerCallDetails, - ) -> grpc.RpcMethodHandler: - handler = continuation(handler_call_details) + handler = await continuation(handler_call_details) if handler is None: return None original_behavior = handler.unary_unary - def new_behavior(request: Any, context: grpc.ServicerContext) -> Any: + async def new_behavior(request: Any, context: grpc.aio.ServicerContext) -> Any: + latency_ms = int((time.monotonic() - start) * 1000) try: - return original_behavior(request, context) # type: ignore[misc] + response = await _async_wrapper(original_behavior, request, context) + logger.info( + "grpc_request", + method=method, + latency_ms=latency_ms, + status="ok", + ) + return response + except Exception as exc: + logger.error( + "grpc_request_error", + method=method, + latency_ms=latency_ms, + error=str(exc), + ) + raise + + handler.unary_unary = new_behavior # type: ignore[method-assign] + return handler + + +class ErrorInterceptor(grpc.aio.ServerInterceptor): + """异常捕获 → gRPC status code 映射(异步).""" + + async def intercept_service( + self, + continuation: Any, + handler_call_details: grpc.HandlerCallDetails, + ) -> grpc.RpcMethodHandler: + handler = await continuation(handler_call_details) + if handler is None: + return None + + original_behavior = handler.unary_unary + + async def new_behavior(request: Any, context: grpc.aio.ServicerContext) -> Any: + try: + return await _async_wrapper(original_behavior, request, context) except AIError as exc: code, msg, grpc_status = grpc_error_mapper(exc) logger.warning( @@ -88,7 +96,7 @@ class ErrorInterceptor(grpc.ServerInterceptor): error_code=code, message=msg, ) - context.abort(_grpc_status(grpc_status), f"{code}: {msg}") + await context.abort(_grpc_status(grpc_status), f"{code}: {msg}") except Exception as exc: code, msg, grpc_status = grpc_error_mapper(exc) logger.error( @@ -96,31 +104,33 @@ class ErrorInterceptor(grpc.ServerInterceptor): method=handler_call_details.method, error=str(exc), ) - context.abort(_grpc_status(grpc_status), f"{code}: {msg}") + await context.abort(_grpc_status(grpc_status), f"{code}: {msg}") handler.unary_unary = new_behavior # type: ignore[method-assign] return handler -class AuthInterceptor(grpc.ServerInterceptor): - """从 gRPC metadata 提取用户上下文,存入 context.""" +class AuthInterceptor(grpc.aio.ServerInterceptor): + """从 gRPC metadata 提取用户上下文,存入 context(异步).""" - def intercept_service( + async def intercept_service( self, - continuation: Callable[[grpc.HandlerCallDetails], grpc.RpcMethodHandler], + continuation: Any, handler_call_details: grpc.HandlerCallDetails, ) -> grpc.RpcMethodHandler: - handler = continuation(handler_call_details) + handler = await continuation(handler_call_details) if handler is None: return None original_behavior = handler.unary_unary - def new_behavior(request: Any, context: grpc.ServicerContext) -> Any: - ctx = extract_user_context_from_metadata(handler_call_details.invocation_metadata) + async def new_behavior(request: Any, context: grpc.aio.ServicerContext) -> Any: + ctx = extract_user_context_from_metadata( + handler_call_details.invocation_metadata, + ) # 将 UserContext 存入 context 供 servicer 使用 context.user_context = ctx # type: ignore[attr-defined] - return original_behavior(request, context) # type: ignore[misc] + return await _async_wrapper(original_behavior, request, context) handler.unary_unary = new_behavior # type: ignore[method-assign] return handler @@ -149,6 +159,6 @@ def _grpc_status(code: int) -> grpc.StatusCode: return status_map.get(code, grpc.StatusCode.UNKNOWN) -def get_user_context(context: grpc.ServicerContext) -> UserContext: +def get_user_context(context: grpc.aio.ServicerContext) -> UserContext: """从 ServicerContext 提取 UserContext(AuthInterceptor 注入).""" return getattr(context, "user_context", UserContext()) diff --git a/services/ai/src/ai/grpc_server/server.py b/services/ai/src/ai/grpc_server/server.py index d246a08..1f96393 100644 --- a/services/ai/src/ai/grpc_server/server.py +++ b/services/ai/src/ai/grpc_server/server.py @@ -39,16 +39,10 @@ class GrpcServer: async def start(self) -> None: """启动 gRPC server. - 注意:grpc.aio.server 的 interceptors 需要 grpc.aio.ServerInterceptor 基类, - 当前拦截器使用同步 grpc.ServerInterceptor 基类会报 ValueError。 - 此处 try/except 降级为无拦截器启动,避免阻塞服务启动。 + 使用 grpc.aio.ServerInterceptor 异步拦截器(Logging/Auth/Error)。 """ interceptors = [LoggingInterceptor(), AuthInterceptor(), ErrorInterceptor()] - try: - self._server = grpc.aio.server(interceptors=interceptors) - except (ValueError, TypeError): - logger.warning("grpc_interceptors_incompatible_start_without") - self._server = grpc.aio.server() + self._server = grpc.aio.server(interceptors=interceptors) ai_pb2_grpc.add_AiServiceServicer_to_server(self._servicer, self._server) self._server.add_insecure_port(f"[::]:{self._port}") await self._server.start() diff --git a/services/ai/src/ai/main.py b/services/ai/src/ai/main.py index 8cb898c..9ac81a4 100644 --- a/services/ai/src/ai/main.py +++ b/services/ai/src/ai/main.py @@ -8,7 +8,7 @@ - 评估三道防线(RuleValidator + LLMJudge + QualityGate) - 用量记录(Redis)+ Kafka 事件发布 + 配额管理 - 安全层(PII + 输入清洗 + 输出审核) - - 下游 gRPC 客户端(content/data-ana/iam,全并行用 Mock) + - 下游 gRPC 客户端(content/data-ana/iam,真实 gRPC 调用) - 备课工作流(4 步编排 + Redis 状态存储) - 限流(Redis 三维度令牌桶) - OpenTelemetry + Prometheus @@ -30,7 +30,7 @@ from opentelemetry.sdk.trace.export import BatchSpanProcessor from prometheus_client import make_asgi_app from redis.asyncio import Redis -from .clients import ContentClientMock, DataAnaClientMock, IamClientMock +from .clients import ContentClientGrpc, DataAnaClientGrpc, IamClientGrpc from .config import settings from .grpc_server import create_grpc_server from .middleware import ( @@ -130,10 +130,11 @@ _rate_limiter = RateLimiter( school_limit=settings.redis_rate_limit_school_per_min, ) -# 下游客户端(全并行模式用 Mock,ISSUE-07) -_content_client = ContentClientMock() -_data_ana_client = DataAnaClientMock() -_iam_client = IamClientMock() +# 下游 gRPC 客户端(真实 gRPC 调用,lifespan 中连接) +# 连接失败时降级(不阻断启动),但调用未连接的客户端方法会抛 AIError +_content_client = ContentClientGrpc(endpoint=settings.content_grpc_endpoint) +_data_ana_client = DataAnaClientGrpc(endpoint=settings.data_ana_grpc_endpoint) +_iam_client = IamClientGrpc(endpoint=settings.iam_grpc_endpoint) _state_store = WorkflowStateStore( redis=None, @@ -179,6 +180,22 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: logger.warning("redis_connect_failed_degraded", error=str(exc)) _redis = None + # 下游 gRPC 客户端连接(每个独立 try,单个失败不阻断其他) + downstream_clients = [ + ("content", _content_client), + ("data_ana", _data_ana_client), + ("iam", _iam_client), + ] + for name, client in downstream_clients: + try: + await client.connect() + except Exception as exc: # noqa: BLE001 + logger.warning( + "downstream_client_connect_failed", + client=name, + error=str(exc), + ) + await _kafka_producer.start() await _grpc_server.start() @@ -188,6 +205,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: grpc_port=settings.grpc_port, dev_mode=settings.is_dev, llm_available=settings.llm_available, + content_connected=_content_client.is_available(), + data_ana_connected=_data_ana_client.is_available(), + iam_connected=_iam_client.is_available(), ) if not settings.llm_available: logger.warning("ai_service_llm_degraded_no_api_key") @@ -198,6 +218,15 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: logger.info("ai_service_stopping") await _grpc_server.stop() await _kafka_producer.stop() + for name, client in reversed(downstream_clients): + try: + await client.close() + except Exception as exc: # noqa: BLE001 + logger.warning( + "downstream_client_close_failed", + client=name, + error=str(exc), + ) if _redis is not None: await _redis.aclose() logger.info("redis_closed") diff --git a/services/ai/src/ai/proto_gen/__init__.py b/services/ai/src/ai/proto_gen/__init__.py index d453134..43bc290 100644 --- a/services/ai/src/ai/proto_gen/__init__.py +++ b/services/ai/src/ai/proto_gen/__init__.py @@ -1,14 +1,46 @@ """protobuf 生成代码(勿手动编辑). -由 grpc_tools.protoc 从 packages/shared-proto/proto/ai.proto 生成。 -重新生成命令: +由 grpc_tools.protoc 从 packages/shared-proto/proto/ 生成。 +重新生成命令(在 services/ai 目录下): uv run python -m grpc_tools.protoc \\ -I ../../packages/shared-proto/proto \\ --python_out=src/ai/proto_gen \\ --grpc_python_out=src/ai/proto_gen \\ - ../../packages/shared-proto/proto/ai.proto + ../../packages/shared-proto/proto/ai.proto \\ + ../../packages/shared-proto/proto/content.proto \\ + ../../packages/shared-proto/proto/analytics.proto \\ + ../../packages/shared-proto/proto/iam.proto + +注意:生成的 *_pb2_grpc.py 文件使用绝对导入(如 `import content_pb2 as content__pb2`), +本文件通过 sys.path 注入确保这些绝对导入可用。 """ -from . import ai_pb2, ai_pb2_grpc +import os +import sys -__all__ = ["ai_pb2", "ai_pb2_grpc"] +# 将本目录加入 sys.path,使生成的 *_pb2_grpc.py 中的绝对导入(如 `import content_pb2`)可用 +_PB_DIR = os.path.dirname(os.path.abspath(__file__)) +if _PB_DIR not in sys.path: + sys.path.insert(0, _PB_DIR) + +from . import ( # noqa: E402 + ai_pb2, + ai_pb2_grpc, + analytics_pb2, + analytics_pb2_grpc, + content_pb2, + content_pb2_grpc, + iam_pb2, + iam_pb2_grpc, +) + +__all__ = [ + "ai_pb2", + "ai_pb2_grpc", + "analytics_pb2", + "analytics_pb2_grpc", + "content_pb2", + "content_pb2_grpc", + "iam_pb2", + "iam_pb2_grpc", +] diff --git a/services/ai/src/ai/proto_gen/analytics_pb2.py b/services/ai/src/ai/proto_gen/analytics_pb2.py new file mode 100644 index 0000000..3ac0698 --- /dev/null +++ b/services/ai/src/ai/proto_gen/analytics_pb2.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: analytics.proto +# Protobuf Python Version: 7.35.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 0, + '', + 'analytics.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61nalytics.proto\x12\x1bnext_edu_cloud.analytics.v1\"h\n\x1aGetClassPerformanceRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\x12\x12\n\nstart_date\x18\x03 \x01(\x03\x12\x10\n\x08\x65nd_date\x18\x04 \x01(\x03\"C\n\x19GetStudentWeaknessRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\"g\n\x17GetLearningTrendRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x12\n\nstart_date\x18\x02 \x01(\x03\x12\x10\n\x08\x65nd_date\x18\x03 \x01(\x03\x12\x12\n\nsubject_id\x18\x04 \x01(\t\"?\n\x1aGetTeacherDashboardRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x02 \x01(\t\"-\n\x1aGetStudentDashboardRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"@\n\x19GetParentDashboardRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x12\n\nstudent_id\x18\x02 \x01(\t\"L\n\x18GetAdminDashboardRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\r\n\x05scope\x18\x02 \x01(\t\x12\x10\n\x08scope_id\x18\x03 \x01(\t\"G\n\x12GetWarningsRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x10\n\x08severity\x18\x02 \x01(\t\x12\r\n\x05since\x18\x03 \x01(\x03\"R\n\x15TriggerWarningRequest\x12\x11\n\ttarget_id\x18\x01 \x01(\t\x12\x14\n\x0cwarning_type\x18\x02 \x01(\t\x12\x10\n\x08severity\x18\x03 \x01(\t\"a\n\x1dGetMasteryDistributionRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\x12\x1a\n\x12knowledge_point_id\x18\x03 \x01(\t\"B\n\x18GetStudentMasteryRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\"E\n\x1dSubscribeMasteryUpdateRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x02 \x01(\t\"\xa1\x01\n\x10\x43lassPerformance\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x15\n\raverage_score\x18\x02 \x01(\x01\x12\x11\n\tpass_rate\x18\x03 \x01(\x01\x12\x16\n\x0etotal_students\x18\x04 \x01(\x05\x12\x39\n\x06scores\x18\x05 \x03(\x0b\x32).next_edu_cloud.analytics.v1.StudentScore\"@\n\x0cStudentScore\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\r\n\x05score\x18\x02 \x01(\x01\x12\r\n\x05grade\x18\x03 \x01(\t\"b\n\x0fStudentWeakness\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12;\n\x0bweak_points\x18\x02 \x03(\x0b\x32&.next_edu_cloud.analytics.v1.WeakPoint\"\\\n\tWeakPoint\x12\x1a\n\x12knowledge_point_id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0f\n\x07mastery\x18\x03 \x01(\x01\x12\x13\n\x0b\x65rror_count\x18\x04 \x01(\x05\"\\\n\rLearningTrend\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x37\n\x06points\x18\x02 \x03(\x0b\x32\'.next_edu_cloud.analytics.v1.TrendPoint\")\n\nTrendPoint\x12\x0c\n\x04\x64\x61te\x18\x01 \x01(\x03\x12\r\n\x05score\x18\x02 \x01(\x01\"\xcd\x02\n\x10TeacherDashboard\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x15\n\rtotal_classes\x18\x02 \x01(\x05\x12\x16\n\x0etotal_students\x18\x03 \x01(\x05\x12\x17\n\x0f\x63lass_avg_score\x18\x04 \x01(\x01\x12\x1e\n\x16pending_homework_count\x18\x05 \x01(\x05\x12:\n\x07\x63lasses\x18\x06 \x03(\x0b\x32).next_edu_cloud.analytics.v1.ClassSummary\x12\x41\n\x0ctop_students\x18\x07 \x03(\x0b\x32+.next_edu_cloud.analytics.v1.StudentSummary\x12\x41\n\x0frecent_warnings\x18\x08 \x03(\x0b\x32(.next_edu_cloud.analytics.v1.WarningInfo\"b\n\x0c\x43lassSummary\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x12\n\nclass_name\x18\x02 \x01(\t\x12\x15\n\rstudent_count\x18\x03 \x01(\x05\x12\x15\n\raverage_score\x18\x04 \x01(\x01\"`\n\x0eStudentSummary\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x14\n\x0cstudent_name\x18\x02 \x01(\t\x12\r\n\x05score\x18\x03 \x01(\x01\x12\x15\n\rrank_in_class\x18\x04 \x01(\x05\"\xf9\x01\n\x10StudentDashboard\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x11\n\tavg_score\x18\x02 \x01(\x01\x12\x12\n\nclass_rank\x18\x03 \x01(\x05\x12\x16\n\x0etotal_students\x18\x04 \x01(\x05\x12;\n\x0bweak_points\x18\x05 \x03(\x0b\x32&.next_edu_cloud.analytics.v1.WeakPoint\x12>\n\rrecent_trends\x18\x06 \x03(\x0b\x32\'.next_edu_cloud.analytics.v1.TrendPoint\x12\x18\n\x10pending_homework\x18\x07 \x01(\x05\"\x8c\x02\n\x0fParentDashboard\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x12\n\nstudent_id\x18\x02 \x01(\t\x12\x17\n\x0f\x63hild_avg_score\x18\x03 \x01(\x01\x12\x18\n\x10\x63hild_class_rank\x18\x04 \x01(\x05\x12\x1c\n\x14total_class_students\x18\x05 \x01(\x05\x12\x41\n\x11\x63hild_weak_points\x18\x06 \x03(\x0b\x32&.next_edu_cloud.analytics.v1.WeakPoint\x12@\n\x0e\x63hild_warnings\x18\x07 \x03(\x0b\x32(.next_edu_cloud.analytics.v1.WarningInfo\"\x84\x02\n\x0e\x41\x64minDashboard\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x16\n\x0etotal_teachers\x18\x02 \x01(\x05\x12\x16\n\x0etotal_students\x18\x03 \x01(\x05\x12\x15\n\rtotal_classes\x18\x04 \x01(\x05\x12\x18\n\x10school_avg_score\x18\x05 \x01(\x01\x12\x41\n\x0frecent_warnings\x18\x06 \x03(\x0b\x32(.next_edu_cloud.analytics.v1.WarningInfo\x12=\n\x08\x61i_usage\x18\x07 \x01(\x0b\x32+.next_edu_cloud.analytics.v1.AIUsageSummary\"\x9d\x01\n\x0e\x41IUsageSummary\x12\x16\n\x0etotal_requests\x18\x01 \x01(\x03\x12\x14\n\x0ctotal_tokens\x18\x02 \x01(\x03\x12\x18\n\x10total_cost_cents\x18\x03 \x01(\x03\x12\x43\n\x0b\x62y_provider\x18\x04 \x03(\x0b\x32..next_edu_cloud.analytics.v1.AIUsageByProvider\"f\n\x11\x41IUsageByProvider\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\x15\n\rrequest_count\x18\x02 \x01(\x03\x12\x14\n\x0ctotal_tokens\x18\x03 \x01(\x03\x12\x12\n\ncost_cents\x18\x04 \x01(\x03\"X\n\x0bWarningList\x12:\n\x08warnings\x18\x01 \x03(\x0b\x32(.next_edu_cloud.analytics.v1.WarningInfo\x12\r\n\x05total\x18\x02 \x01(\x05\"\xb0\x01\n\x0bWarningInfo\x12\x12\n\nwarning_id\x18\x01 \x01(\t\x12\x14\n\x0cwarning_type\x18\x02 \x01(\t\x12\x11\n\ttarget_id\x18\x03 \x01(\t\x12\x13\n\x0btarget_name\x18\x04 \x01(\t\x12\x11\n\tthreshold\x18\x05 \x01(\x01\x12\x15\n\rcurrent_value\x18\x06 \x01(\x01\x12\x10\n\x08severity\x18\x07 \x01(\t\x12\x13\n\x0boccurred_at\x18\x08 \x01(\x03\"?\n\x16TriggerWarningResponse\x12\x12\n\nwarning_id\x18\x01 \x01(\t\x12\x11\n\ttriggered\x18\x02 \x01(\x08\"\x86\x01\n\x13MasteryDistribution\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x16\n\x0emastered_count\x18\x02 \x01(\x05\x12\x19\n\x11progressing_count\x18\x03 \x01(\x05\x12\x12\n\nweak_count\x18\x04 \x01(\x05\x12\x16\n\x0etotal_students\x18\x05 \x01(\x05\"\x8b\x01\n\x0eStudentMastery\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12L\n\x10knowledge_points\x18\x02 \x03(\x0b\x32\x32.next_edu_cloud.analytics.v1.KnowledgePointMastery\x12\x17\n\x0foverall_mastery\x18\x03 \x01(\x01\"\x9b\x01\n\x15KnowledgePointMastery\x12\x1a\n\x12knowledge_point_id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x12\n\nsubject_id\x18\x03 \x01(\t\x12\x15\n\rmastery_level\x18\x04 \x01(\x01\x12\x15\n\rmastery_label\x18\x05 \x01(\t\x12\x15\n\rcalculated_at\x18\x06 \x01(\x03\"\x9c\x01\n\x12MasteryUpdateEvent\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\t\x12\x12\n\nstudent_id\x18\x02 \x01(\t\x12\x1a\n\x12knowledge_point_id\x18\x03 \x01(\t\x12\x15\n\rmastery_level\x18\x04 \x01(\x01\x12\x16\n\x0eprevious_level\x18\x05 \x01(\x01\x12\x15\n\rcalculated_at\x18\x06 \x01(\x03\x32\xe7\x0b\n\x10\x41nalyticsService\x12}\n\x13GetClassPerformance\x12\x37.next_edu_cloud.analytics.v1.GetClassPerformanceRequest\x1a-.next_edu_cloud.analytics.v1.ClassPerformance\x12z\n\x12GetStudentWeakness\x12\x36.next_edu_cloud.analytics.v1.GetStudentWeaknessRequest\x1a,.next_edu_cloud.analytics.v1.StudentWeakness\x12t\n\x10GetLearningTrend\x12\x34.next_edu_cloud.analytics.v1.GetLearningTrendRequest\x1a*.next_edu_cloud.analytics.v1.LearningTrend\x12}\n\x13GetTeacherDashboard\x12\x37.next_edu_cloud.analytics.v1.GetTeacherDashboardRequest\x1a-.next_edu_cloud.analytics.v1.TeacherDashboard\x12}\n\x13GetStudentDashboard\x12\x37.next_edu_cloud.analytics.v1.GetStudentDashboardRequest\x1a-.next_edu_cloud.analytics.v1.StudentDashboard\x12z\n\x12GetParentDashboard\x12\x36.next_edu_cloud.analytics.v1.GetParentDashboardRequest\x1a,.next_edu_cloud.analytics.v1.ParentDashboard\x12w\n\x11GetAdminDashboard\x12\x35.next_edu_cloud.analytics.v1.GetAdminDashboardRequest\x1a+.next_edu_cloud.analytics.v1.AdminDashboard\x12h\n\x0bGetWarnings\x12/.next_edu_cloud.analytics.v1.GetWarningsRequest\x1a(.next_edu_cloud.analytics.v1.WarningList\x12y\n\x0eTriggerWarning\x12\x32.next_edu_cloud.analytics.v1.TriggerWarningRequest\x1a\x33.next_edu_cloud.analytics.v1.TriggerWarningResponse\x12\x86\x01\n\x16GetMasteryDistribution\x12:.next_edu_cloud.analytics.v1.GetMasteryDistributionRequest\x1a\x30.next_edu_cloud.analytics.v1.MasteryDistribution\x12w\n\x11GetStudentMastery\x12\x35.next_edu_cloud.analytics.v1.GetStudentMasteryRequest\x1a+.next_edu_cloud.analytics.v1.StudentMastery\x12\x87\x01\n\x16SubscribeMasteryUpdate\x12:.next_edu_cloud.analytics.v1.SubscribeMasteryUpdateRequest\x1a/.next_edu_cloud.analytics.v1.MasteryUpdateEvent0\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'analytics_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_GETCLASSPERFORMANCEREQUEST']._serialized_start=48 + _globals['_GETCLASSPERFORMANCEREQUEST']._serialized_end=152 + _globals['_GETSTUDENTWEAKNESSREQUEST']._serialized_start=154 + _globals['_GETSTUDENTWEAKNESSREQUEST']._serialized_end=221 + _globals['_GETLEARNINGTRENDREQUEST']._serialized_start=223 + _globals['_GETLEARNINGTRENDREQUEST']._serialized_end=326 + _globals['_GETTEACHERDASHBOARDREQUEST']._serialized_start=328 + _globals['_GETTEACHERDASHBOARDREQUEST']._serialized_end=391 + _globals['_GETSTUDENTDASHBOARDREQUEST']._serialized_start=393 + _globals['_GETSTUDENTDASHBOARDREQUEST']._serialized_end=438 + _globals['_GETPARENTDASHBOARDREQUEST']._serialized_start=440 + _globals['_GETPARENTDASHBOARDREQUEST']._serialized_end=504 + _globals['_GETADMINDASHBOARDREQUEST']._serialized_start=506 + _globals['_GETADMINDASHBOARDREQUEST']._serialized_end=582 + _globals['_GETWARNINGSREQUEST']._serialized_start=584 + _globals['_GETWARNINGSREQUEST']._serialized_end=655 + _globals['_TRIGGERWARNINGREQUEST']._serialized_start=657 + _globals['_TRIGGERWARNINGREQUEST']._serialized_end=739 + _globals['_GETMASTERYDISTRIBUTIONREQUEST']._serialized_start=741 + _globals['_GETMASTERYDISTRIBUTIONREQUEST']._serialized_end=838 + _globals['_GETSTUDENTMASTERYREQUEST']._serialized_start=840 + _globals['_GETSTUDENTMASTERYREQUEST']._serialized_end=906 + _globals['_SUBSCRIBEMASTERYUPDATEREQUEST']._serialized_start=908 + _globals['_SUBSCRIBEMASTERYUPDATEREQUEST']._serialized_end=977 + _globals['_CLASSPERFORMANCE']._serialized_start=980 + _globals['_CLASSPERFORMANCE']._serialized_end=1141 + _globals['_STUDENTSCORE']._serialized_start=1143 + _globals['_STUDENTSCORE']._serialized_end=1207 + _globals['_STUDENTWEAKNESS']._serialized_start=1209 + _globals['_STUDENTWEAKNESS']._serialized_end=1307 + _globals['_WEAKPOINT']._serialized_start=1309 + _globals['_WEAKPOINT']._serialized_end=1401 + _globals['_LEARNINGTREND']._serialized_start=1403 + _globals['_LEARNINGTREND']._serialized_end=1495 + _globals['_TRENDPOINT']._serialized_start=1497 + _globals['_TRENDPOINT']._serialized_end=1538 + _globals['_TEACHERDASHBOARD']._serialized_start=1541 + _globals['_TEACHERDASHBOARD']._serialized_end=1874 + _globals['_CLASSSUMMARY']._serialized_start=1876 + _globals['_CLASSSUMMARY']._serialized_end=1974 + _globals['_STUDENTSUMMARY']._serialized_start=1976 + _globals['_STUDENTSUMMARY']._serialized_end=2072 + _globals['_STUDENTDASHBOARD']._serialized_start=2075 + _globals['_STUDENTDASHBOARD']._serialized_end=2324 + _globals['_PARENTDASHBOARD']._serialized_start=2327 + _globals['_PARENTDASHBOARD']._serialized_end=2595 + _globals['_ADMINDASHBOARD']._serialized_start=2598 + _globals['_ADMINDASHBOARD']._serialized_end=2858 + _globals['_AIUSAGESUMMARY']._serialized_start=2861 + _globals['_AIUSAGESUMMARY']._serialized_end=3018 + _globals['_AIUSAGEBYPROVIDER']._serialized_start=3020 + _globals['_AIUSAGEBYPROVIDER']._serialized_end=3122 + _globals['_WARNINGLIST']._serialized_start=3124 + _globals['_WARNINGLIST']._serialized_end=3212 + _globals['_WARNINGINFO']._serialized_start=3215 + _globals['_WARNINGINFO']._serialized_end=3391 + _globals['_TRIGGERWARNINGRESPONSE']._serialized_start=3393 + _globals['_TRIGGERWARNINGRESPONSE']._serialized_end=3456 + _globals['_MASTERYDISTRIBUTION']._serialized_start=3459 + _globals['_MASTERYDISTRIBUTION']._serialized_end=3593 + _globals['_STUDENTMASTERY']._serialized_start=3596 + _globals['_STUDENTMASTERY']._serialized_end=3735 + _globals['_KNOWLEDGEPOINTMASTERY']._serialized_start=3738 + _globals['_KNOWLEDGEPOINTMASTERY']._serialized_end=3893 + _globals['_MASTERYUPDATEEVENT']._serialized_start=3896 + _globals['_MASTERYUPDATEEVENT']._serialized_end=4052 + _globals['_ANALYTICSSERVICE']._serialized_start=4055 + _globals['_ANALYTICSSERVICE']._serialized_end=5566 +# @@protoc_insertion_point(module_scope) diff --git a/services/ai/src/ai/proto_gen/analytics_pb2_grpc.py b/services/ai/src/ai/proto_gen/analytics_pb2_grpc.py new file mode 100644 index 0000000..aa97e6a --- /dev/null +++ b/services/ai/src/ai/proto_gen/analytics_pb2_grpc.py @@ -0,0 +1,591 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import analytics_pb2 as analytics__pb2 + +GRPC_GENERATED_VERSION = '1.82.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in analytics_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class AnalyticsServiceStub: + """AnalyticsService 数据分析服务契约(D6 智能洞察领域). + P4 启用 gRPC server 端口 50055,HTTP 3006 保留作 Gateway 直连降级. + 所有 RPC 返回 ActionState 信封(success/data/error/details.degraded). + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetClassPerformance = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetClassPerformance', + request_serializer=analytics__pb2.GetClassPerformanceRequest.SerializeToString, + response_deserializer=analytics__pb2.ClassPerformance.FromString, + _registered_method=True) + self.GetStudentWeakness = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentWeakness', + request_serializer=analytics__pb2.GetStudentWeaknessRequest.SerializeToString, + response_deserializer=analytics__pb2.StudentWeakness.FromString, + _registered_method=True) + self.GetLearningTrend = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetLearningTrend', + request_serializer=analytics__pb2.GetLearningTrendRequest.SerializeToString, + response_deserializer=analytics__pb2.LearningTrend.FromString, + _registered_method=True) + self.GetTeacherDashboard = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetTeacherDashboard', + request_serializer=analytics__pb2.GetTeacherDashboardRequest.SerializeToString, + response_deserializer=analytics__pb2.TeacherDashboard.FromString, + _registered_method=True) + self.GetStudentDashboard = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentDashboard', + request_serializer=analytics__pb2.GetStudentDashboardRequest.SerializeToString, + response_deserializer=analytics__pb2.StudentDashboard.FromString, + _registered_method=True) + self.GetParentDashboard = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetParentDashboard', + request_serializer=analytics__pb2.GetParentDashboardRequest.SerializeToString, + response_deserializer=analytics__pb2.ParentDashboard.FromString, + _registered_method=True) + self.GetAdminDashboard = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetAdminDashboard', + request_serializer=analytics__pb2.GetAdminDashboardRequest.SerializeToString, + response_deserializer=analytics__pb2.AdminDashboard.FromString, + _registered_method=True) + self.GetWarnings = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetWarnings', + request_serializer=analytics__pb2.GetWarningsRequest.SerializeToString, + response_deserializer=analytics__pb2.WarningList.FromString, + _registered_method=True) + self.TriggerWarning = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/TriggerWarning', + request_serializer=analytics__pb2.TriggerWarningRequest.SerializeToString, + response_deserializer=analytics__pb2.TriggerWarningResponse.FromString, + _registered_method=True) + self.GetMasteryDistribution = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetMasteryDistribution', + request_serializer=analytics__pb2.GetMasteryDistributionRequest.SerializeToString, + response_deserializer=analytics__pb2.MasteryDistribution.FromString, + _registered_method=True) + self.GetStudentMastery = channel.unary_unary( + '/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentMastery', + request_serializer=analytics__pb2.GetStudentMasteryRequest.SerializeToString, + response_deserializer=analytics__pb2.StudentMastery.FromString, + _registered_method=True) + self.SubscribeMasteryUpdate = channel.unary_stream( + '/next_edu_cloud.analytics.v1.AnalyticsService/SubscribeMasteryUpdate', + request_serializer=analytics__pb2.SubscribeMasteryUpdateRequest.SerializeToString, + response_deserializer=analytics__pb2.MasteryUpdateEvent.FromString, + _registered_method=True) + + +class AnalyticsServiceServicer: + """AnalyticsService 数据分析服务契约(D6 智能洞察领域). + P4 启用 gRPC server 端口 50055,HTTP 3006 保留作 Gateway 直连降级. + 所有 RPC 返回 ActionState 信封(success/data/error/details.degraded). + """ + + def GetClassPerformance(self, request, context): + """班级成绩分析(平均分/及格率/参考人数). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetStudentWeakness(self, request, context): + """学生薄弱知识点(mastery_level < 0.6). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLearningTrend(self, request, context): + """学习趋势(历史成绩曲线). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTeacherDashboard(self, request, context): + """教师仪表盘聚合(班级概览 + 待办 + 预警). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetStudentDashboard(self, request, context): + """学生仪表盘(个人学情 + 排名 + 薄弱点). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetParentDashboard(self, request, context): + """家长仪表盘(孩子学情概览). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetAdminDashboard(self, request, context): + """管理员仪表盘(全校统计 + AI 用量). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetWarnings(self, request, context): + """预警列表查询(按班级/严重度/时间过滤). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def TriggerWarning(self, request, context): + """手动触发预警(管理员/教师主动标记关注). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetMasteryDistribution(self, request, context): + """班级掌握度分布(mastered/progressing/weak 三档). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetStudentMastery(self, request, context): + """学生知识点掌握度明细. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SubscribeMasteryUpdate(self, request, context): + """订阅掌握度更新(server-streaming,P5+ AI 个性化推荐实时推送通道). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_AnalyticsServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetClassPerformance': grpc.unary_unary_rpc_method_handler( + servicer.GetClassPerformance, + request_deserializer=analytics__pb2.GetClassPerformanceRequest.FromString, + response_serializer=analytics__pb2.ClassPerformance.SerializeToString, + ), + 'GetStudentWeakness': grpc.unary_unary_rpc_method_handler( + servicer.GetStudentWeakness, + request_deserializer=analytics__pb2.GetStudentWeaknessRequest.FromString, + response_serializer=analytics__pb2.StudentWeakness.SerializeToString, + ), + 'GetLearningTrend': grpc.unary_unary_rpc_method_handler( + servicer.GetLearningTrend, + request_deserializer=analytics__pb2.GetLearningTrendRequest.FromString, + response_serializer=analytics__pb2.LearningTrend.SerializeToString, + ), + 'GetTeacherDashboard': grpc.unary_unary_rpc_method_handler( + servicer.GetTeacherDashboard, + request_deserializer=analytics__pb2.GetTeacherDashboardRequest.FromString, + response_serializer=analytics__pb2.TeacherDashboard.SerializeToString, + ), + 'GetStudentDashboard': grpc.unary_unary_rpc_method_handler( + servicer.GetStudentDashboard, + request_deserializer=analytics__pb2.GetStudentDashboardRequest.FromString, + response_serializer=analytics__pb2.StudentDashboard.SerializeToString, + ), + 'GetParentDashboard': grpc.unary_unary_rpc_method_handler( + servicer.GetParentDashboard, + request_deserializer=analytics__pb2.GetParentDashboardRequest.FromString, + response_serializer=analytics__pb2.ParentDashboard.SerializeToString, + ), + 'GetAdminDashboard': grpc.unary_unary_rpc_method_handler( + servicer.GetAdminDashboard, + request_deserializer=analytics__pb2.GetAdminDashboardRequest.FromString, + response_serializer=analytics__pb2.AdminDashboard.SerializeToString, + ), + 'GetWarnings': grpc.unary_unary_rpc_method_handler( + servicer.GetWarnings, + request_deserializer=analytics__pb2.GetWarningsRequest.FromString, + response_serializer=analytics__pb2.WarningList.SerializeToString, + ), + 'TriggerWarning': grpc.unary_unary_rpc_method_handler( + servicer.TriggerWarning, + request_deserializer=analytics__pb2.TriggerWarningRequest.FromString, + response_serializer=analytics__pb2.TriggerWarningResponse.SerializeToString, + ), + 'GetMasteryDistribution': grpc.unary_unary_rpc_method_handler( + servicer.GetMasteryDistribution, + request_deserializer=analytics__pb2.GetMasteryDistributionRequest.FromString, + response_serializer=analytics__pb2.MasteryDistribution.SerializeToString, + ), + 'GetStudentMastery': grpc.unary_unary_rpc_method_handler( + servicer.GetStudentMastery, + request_deserializer=analytics__pb2.GetStudentMasteryRequest.FromString, + response_serializer=analytics__pb2.StudentMastery.SerializeToString, + ), + 'SubscribeMasteryUpdate': grpc.unary_stream_rpc_method_handler( + servicer.SubscribeMasteryUpdate, + request_deserializer=analytics__pb2.SubscribeMasteryUpdateRequest.FromString, + response_serializer=analytics__pb2.MasteryUpdateEvent.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.analytics.v1.AnalyticsService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.analytics.v1.AnalyticsService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class AnalyticsService: + """AnalyticsService 数据分析服务契约(D6 智能洞察领域). + P4 启用 gRPC server 端口 50055,HTTP 3006 保留作 Gateway 直连降级. + 所有 RPC 返回 ActionState 信封(success/data/error/details.degraded). + """ + + @staticmethod + def GetClassPerformance(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetClassPerformance', + analytics__pb2.GetClassPerformanceRequest.SerializeToString, + analytics__pb2.ClassPerformance.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetStudentWeakness(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentWeakness', + analytics__pb2.GetStudentWeaknessRequest.SerializeToString, + analytics__pb2.StudentWeakness.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLearningTrend(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetLearningTrend', + analytics__pb2.GetLearningTrendRequest.SerializeToString, + analytics__pb2.LearningTrend.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTeacherDashboard(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetTeacherDashboard', + analytics__pb2.GetTeacherDashboardRequest.SerializeToString, + analytics__pb2.TeacherDashboard.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetStudentDashboard(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentDashboard', + analytics__pb2.GetStudentDashboardRequest.SerializeToString, + analytics__pb2.StudentDashboard.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetParentDashboard(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetParentDashboard', + analytics__pb2.GetParentDashboardRequest.SerializeToString, + analytics__pb2.ParentDashboard.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetAdminDashboard(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetAdminDashboard', + analytics__pb2.GetAdminDashboardRequest.SerializeToString, + analytics__pb2.AdminDashboard.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetWarnings(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetWarnings', + analytics__pb2.GetWarningsRequest.SerializeToString, + analytics__pb2.WarningList.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def TriggerWarning(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/TriggerWarning', + analytics__pb2.TriggerWarningRequest.SerializeToString, + analytics__pb2.TriggerWarningResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetMasteryDistribution(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetMasteryDistribution', + analytics__pb2.GetMasteryDistributionRequest.SerializeToString, + analytics__pb2.MasteryDistribution.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetStudentMastery(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/GetStudentMastery', + analytics__pb2.GetStudentMasteryRequest.SerializeToString, + analytics__pb2.StudentMastery.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def SubscribeMasteryUpdate(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream( + request, + target, + '/next_edu_cloud.analytics.v1.AnalyticsService/SubscribeMasteryUpdate', + analytics__pb2.SubscribeMasteryUpdateRequest.SerializeToString, + analytics__pb2.MasteryUpdateEvent.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/services/ai/src/ai/proto_gen/content_pb2.py b/services/ai/src/ai/proto_gen/content_pb2.py new file mode 100644 index 0000000..fb17b94 --- /dev/null +++ b/services/ai/src/ai/proto_gen/content_pb2.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: content.proto +# Protobuf Python Version: 7.35.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 0, + '', + 'content.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rcontent.proto\x12\x19next_edu_cloud.content.v1\x1a\x1cgoogle/protobuf/struct.proto\"\x07\n\x05\x45mpty\"\xd2\x01\n\x08Textbook\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x12\n\nsubject_id\x18\x03 \x01(\t\x12\x10\n\x08grade_id\x18\x04 \x01(\t\x12\x0f\n\x07version\x18\x05 \x01(\t\x12\x0e\n\x06status\x18\x06 \x01(\t\x12\x11\n\ttenant_id\x18\x07 \x01(\t\x12)\n\x08metadata\x18\x08 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x12\n\ncreated_at\x18\t \x01(\x03\x12\x12\n\nupdated_at\x18\n \x01(\x03\"\x93\x01\n\x07\x43hapter\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0btextbook_id\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\r\n\x05order\x18\x04 \x01(\x05\x12\x11\n\tparent_id\x18\x05 \x01(\t\x12\x0e\n\x06status\x18\x06 \x01(\t\x12\x12\n\ncreated_at\x18\x07 \x01(\x03\x12\x12\n\nupdated_at\x18\x08 \x01(\x03\"\xbb\x01\n\x0eKnowledgePoint\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nchapter_id\x18\x02 \x01(\t\x12\r\n\x05title\x18\x03 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x04 \x01(\t\x12\x12\n\ndifficulty\x18\x05 \x01(\x05\x12)\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x12\n\ncreated_at\x18\x07 \x01(\x03\x12\x12\n\nupdated_at\x18\x08 \x01(\x03\"\xbb\x02\n\x08Question\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1a\n\x12knowledge_point_id\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x04 \x01(\t\x12(\n\x07options\x18\x05 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0e\n\x06\x61nswer\x18\x06 \x01(\t\x12\x13\n\x0b\x65xplanation\x18\x07 \x01(\t\x12\x12\n\ndifficulty\x18\x08 \x01(\x05\x12\x0e\n\x06status\x18\t \x01(\t\x12\x0e\n\x06source\x18\n \x01(\t\x12\x12\n\ncreated_by\x18\x0b \x01(\t\x12)\n\x08metadata\x18\x0c \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x12\n\ncreated_at\x18\r \x01(\x03\x12\x12\n\nupdated_at\x18\x0e \x01(\x03\"\x88\x01\n\x15\x43reateTextbookRequest\x12\r\n\x05title\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\x12\x10\n\x08grade_id\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\t\x12)\n\x08metadata\x18\x05 \x01(\x0b\x32\x17.google.protobuf.Struct\" \n\x12GetTextbookRequest\x12\n\n\x02id\x18\x01 \x01(\t\"c\n\x14ListTextbooksRequest\x12\x12\n\nsubject_id\x18\x01 \x01(\t\x12\x10\n\x08grade_id\x18\x02 \x01(\t\x12\x12\n\npage_token\x18\x03 \x01(\t\x12\x11\n\tpage_size\x18\x04 \x01(\x05\"h\n\x15ListTextbooksResponse\x12\x36\n\ttextbooks\x18\x01 \x03(\x0b\x32#.next_edu_cloud.content.v1.Textbook\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"\x8c\x01\n\x15UpdateTextbookRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\x05title\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06status\x18\x03 \x01(\tH\x01\x88\x01\x01\x12)\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructB\x08\n\x06_titleB\t\n\x07_status\"#\n\x15\x44\x65leteTextbookRequest\x12\n\n\x02id\x18\x01 \x01(\t\"\\\n\x14\x43reateChapterRequest\x12\x13\n\x0btextbook_id\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\r\n\x05order\x18\x03 \x01(\x05\x12\x11\n\tparent_id\x18\x04 \x01(\t\"\x1f\n\x11GetChapterRequest\x12\n\n\x02id\x18\x01 \x01(\t\"=\n\x13ListChaptersRequest\x12\x13\n\x0btextbook_id\x18\x01 \x01(\t\x12\x11\n\tparent_id\x18\x02 \x01(\t\"L\n\x14ListChaptersResponse\x12\x34\n\x08\x63hapters\x18\x01 \x03(\x0b\x32\".next_edu_cloud.content.v1.Chapter\"~\n\x14UpdateChapterRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\x05title\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x12\n\x05order\x18\x03 \x01(\x05H\x01\x88\x01\x01\x12\x13\n\x06status\x18\x04 \x01(\tH\x02\x88\x01\x01\x42\x08\n\x06_titleB\x08\n\x06_orderB\t\n\x07_status\"\"\n\x14\x44\x65leteChapterRequest\x12\n\n\x02id\x18\x01 \x01(\t\"D\n\x17GetPrerequisitesRequest\x12\x1a\n\x12knowledge_point_id\x18\x01 \x01(\t\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\x05\"T\n\x17KnowledgePointsResponse\x12\x39\n\x06points\x18\x01 \x03(\x0b\x32).next_edu_cloud.content.v1.KnowledgePoint\"@\n\x16GetLearningPathRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\"d\n\x0cLearningPath\x12\x39\n\x06points\x18\x01 \x03(\x0b\x32).next_edu_cloud.content.v1.KnowledgePoint\x12\x19\n\x11recommended_order\x18\x02 \x03(\t\"@\n\x16\x41\x64\x64PrerequisiteRequest\x12\r\n\x05kp_id\x18\x01 \x01(\t\x12\x17\n\x0fprerequisite_id\x18\x02 \x01(\t\"C\n\x19RemovePrerequisiteRequest\x12\r\n\x05kp_id\x18\x01 \x01(\t\x12\x17\n\x0fprerequisite_id\x18\x02 \x01(\t\"\x84\x02\n\x15\x43reateQuestionRequest\x12\x1a\n\x12knowledge_point_id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12(\n\x07options\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0e\n\x06\x61nswer\x18\x05 \x01(\t\x12\x13\n\x0b\x65xplanation\x18\x06 \x01(\t\x12\x12\n\ndifficulty\x18\x07 \x01(\x05\x12\x0e\n\x06source\x18\x08 \x01(\t\x12\x12\n\ncreated_by\x18\t \x01(\t\x12)\n\x08metadata\x18\n \x01(\x0b\x32\x17.google.protobuf.Struct\"b\n\x1b\x42\x61tchCreateQuestionsRequest\x12\x43\n\tquestions\x18\x01 \x03(\x0b\x32\x30.next_edu_cloud.content.v1.CreateQuestionRequest\"j\n\x1c\x42\x61tchCreateQuestionsResponse\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12=\n\x06\x66\x61iled\x18\x02 \x03(\x0b\x32-.next_edu_cloud.content.v1.BatchCreateFailure\"2\n\x12\x42\x61tchCreateFailure\x12\r\n\x05index\x18\x01 \x01(\x05\x12\r\n\x05\x65rror\x18\x02 \x01(\t\" \n\x12GetQuestionRequest\x12\n\n\x02id\x18\x01 \x01(\t\"\x8b\x01\n\x14ListQuestionsRequest\x12\x1a\n\x12knowledge_point_id\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x12\n\ndifficulty\x18\x03 \x01(\x05\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x12\n\npage_token\x18\x05 \x01(\t\x12\x11\n\tpage_size\x18\x06 \x01(\x05\"h\n\x15ListQuestionsResponse\x12\x36\n\tquestions\x18\x01 \x03(\x0b\x32#.next_edu_cloud.content.v1.Question\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"\x81\x02\n\x15UpdateQuestionRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x14\n\x07\x63ontent\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x13\n\x06\x61nswer\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x13\n\x06status\x18\x04 \x01(\tH\x02\x88\x01\x01\x12(\n\x07options\x18\x05 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x18\n\x0b\x65xplanation\x18\x06 \x01(\tH\x03\x88\x01\x01\x12\x17\n\ndifficulty\x18\x07 \x01(\x05H\x04\x88\x01\x01\x42\n\n\x08_contentB\t\n\x07_answerB\t\n\x07_statusB\x0e\n\x0c_explanationB\r\n\x0b_difficulty\"#\n\x15\x44\x65leteQuestionRequest\x12\n\n\x02id\x18\x01 \x01(\t\"$\n\x16PublishQuestionRequest\x12\n\n\x02id\x18\x01 \x01(\t\"\x88\x01\n\x16SearchQuestionsRequest\x12\t\n\x01q\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x12\n\ndifficulty\x18\x03 \x01(\x05\x12\x1a\n\x12knowledge_point_id\x18\x04 \x01(\t\x12\x12\n\npage_token\x18\x05 \x01(\t\x12\x11\n\tpage_size\x18\x06 \x01(\x05\"y\n\x17SearchQuestionsResponse\x12\x36\n\tquestions\x18\x01 \x03(\x0b\x32#.next_edu_cloud.content.v1.Question\x12\r\n\x05total\x18\x02 \x01(\x05\x12\x17\n\x0fnext_page_token\x18\x03 \x01(\t2\xa0\x04\n\x0fTextbookService\x12g\n\x0e\x43reateTextbook\x12\x30.next_edu_cloud.content.v1.CreateTextbookRequest\x1a#.next_edu_cloud.content.v1.Textbook\x12\x61\n\x0bGetTextbook\x12-.next_edu_cloud.content.v1.GetTextbookRequest\x1a#.next_edu_cloud.content.v1.Textbook\x12r\n\rListTextbooks\x12/.next_edu_cloud.content.v1.ListTextbooksRequest\x1a\x30.next_edu_cloud.content.v1.ListTextbooksResponse\x12g\n\x0eUpdateTextbook\x12\x30.next_edu_cloud.content.v1.UpdateTextbookRequest\x1a#.next_edu_cloud.content.v1.Textbook\x12\x64\n\x0e\x44\x65leteTextbook\x12\x30.next_edu_cloud.content.v1.DeleteTextbookRequest\x1a .next_edu_cloud.content.v1.Empty2\x91\x04\n\x0e\x43hapterService\x12\x64\n\rCreateChapter\x12/.next_edu_cloud.content.v1.CreateChapterRequest\x1a\".next_edu_cloud.content.v1.Chapter\x12^\n\nGetChapter\x12,.next_edu_cloud.content.v1.GetChapterRequest\x1a\".next_edu_cloud.content.v1.Chapter\x12o\n\x0cListChapters\x12..next_edu_cloud.content.v1.ListChaptersRequest\x1a/.next_edu_cloud.content.v1.ListChaptersResponse\x12\x64\n\rUpdateChapter\x12/.next_edu_cloud.content.v1.UpdateChapterRequest\x1a\".next_edu_cloud.content.v1.Chapter\x12\x62\n\rDeleteChapter\x12/.next_edu_cloud.content.v1.DeleteChapterRequest\x1a .next_edu_cloud.content.v1.Empty2\xd8\x03\n\x15KnowledgeGraphService\x12z\n\x10GetPrerequisites\x12\x32.next_edu_cloud.content.v1.GetPrerequisitesRequest\x1a\x32.next_edu_cloud.content.v1.KnowledgePointsResponse\x12m\n\x0fGetLearningPath\x12\x31.next_edu_cloud.content.v1.GetLearningPathRequest\x1a\'.next_edu_cloud.content.v1.LearningPath\x12\x66\n\x0f\x41\x64\x64Prerequisite\x12\x31.next_edu_cloud.content.v1.AddPrerequisiteRequest\x1a .next_edu_cloud.content.v1.Empty\x12l\n\x12RemovePrerequisite\x12\x34.next_edu_cloud.content.v1.RemovePrerequisiteRequest\x1a .next_edu_cloud.content.v1.Empty2\x8c\x07\n\x0fQuestionService\x12g\n\x0e\x43reateQuestion\x12\x30.next_edu_cloud.content.v1.CreateQuestionRequest\x1a#.next_edu_cloud.content.v1.Question\x12\x87\x01\n\x14\x42\x61tchCreateQuestions\x12\x36.next_edu_cloud.content.v1.BatchCreateQuestionsRequest\x1a\x37.next_edu_cloud.content.v1.BatchCreateQuestionsResponse\x12\x61\n\x0bGetQuestion\x12-.next_edu_cloud.content.v1.GetQuestionRequest\x1a#.next_edu_cloud.content.v1.Question\x12r\n\rListQuestions\x12/.next_edu_cloud.content.v1.ListQuestionsRequest\x1a\x30.next_edu_cloud.content.v1.ListQuestionsResponse\x12g\n\x0eUpdateQuestion\x12\x30.next_edu_cloud.content.v1.UpdateQuestionRequest\x1a#.next_edu_cloud.content.v1.Question\x12\x64\n\x0e\x44\x65leteQuestion\x12\x30.next_edu_cloud.content.v1.DeleteQuestionRequest\x1a .next_edu_cloud.content.v1.Empty\x12\x66\n\x0fPublishQuestion\x12\x31.next_edu_cloud.content.v1.PublishQuestionRequest\x1a .next_edu_cloud.content.v1.Empty\x12x\n\x0fSearchQuestions\x12\x31.next_edu_cloud.content.v1.SearchQuestionsRequest\x1a\x32.next_edu_cloud.content.v1.SearchQuestionsResponseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'content_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_EMPTY']._serialized_start=74 + _globals['_EMPTY']._serialized_end=81 + _globals['_TEXTBOOK']._serialized_start=84 + _globals['_TEXTBOOK']._serialized_end=294 + _globals['_CHAPTER']._serialized_start=297 + _globals['_CHAPTER']._serialized_end=444 + _globals['_KNOWLEDGEPOINT']._serialized_start=447 + _globals['_KNOWLEDGEPOINT']._serialized_end=634 + _globals['_QUESTION']._serialized_start=637 + _globals['_QUESTION']._serialized_end=952 + _globals['_CREATETEXTBOOKREQUEST']._serialized_start=955 + _globals['_CREATETEXTBOOKREQUEST']._serialized_end=1091 + _globals['_GETTEXTBOOKREQUEST']._serialized_start=1093 + _globals['_GETTEXTBOOKREQUEST']._serialized_end=1125 + _globals['_LISTTEXTBOOKSREQUEST']._serialized_start=1127 + _globals['_LISTTEXTBOOKSREQUEST']._serialized_end=1226 + _globals['_LISTTEXTBOOKSRESPONSE']._serialized_start=1228 + _globals['_LISTTEXTBOOKSRESPONSE']._serialized_end=1332 + _globals['_UPDATETEXTBOOKREQUEST']._serialized_start=1335 + _globals['_UPDATETEXTBOOKREQUEST']._serialized_end=1475 + _globals['_DELETETEXTBOOKREQUEST']._serialized_start=1477 + _globals['_DELETETEXTBOOKREQUEST']._serialized_end=1512 + _globals['_CREATECHAPTERREQUEST']._serialized_start=1514 + _globals['_CREATECHAPTERREQUEST']._serialized_end=1606 + _globals['_GETCHAPTERREQUEST']._serialized_start=1608 + _globals['_GETCHAPTERREQUEST']._serialized_end=1639 + _globals['_LISTCHAPTERSREQUEST']._serialized_start=1641 + _globals['_LISTCHAPTERSREQUEST']._serialized_end=1702 + _globals['_LISTCHAPTERSRESPONSE']._serialized_start=1704 + _globals['_LISTCHAPTERSRESPONSE']._serialized_end=1780 + _globals['_UPDATECHAPTERREQUEST']._serialized_start=1782 + _globals['_UPDATECHAPTERREQUEST']._serialized_end=1908 + _globals['_DELETECHAPTERREQUEST']._serialized_start=1910 + _globals['_DELETECHAPTERREQUEST']._serialized_end=1944 + _globals['_GETPREREQUISITESREQUEST']._serialized_start=1946 + _globals['_GETPREREQUISITESREQUEST']._serialized_end=2014 + _globals['_KNOWLEDGEPOINTSRESPONSE']._serialized_start=2016 + _globals['_KNOWLEDGEPOINTSRESPONSE']._serialized_end=2100 + _globals['_GETLEARNINGPATHREQUEST']._serialized_start=2102 + _globals['_GETLEARNINGPATHREQUEST']._serialized_end=2166 + _globals['_LEARNINGPATH']._serialized_start=2168 + _globals['_LEARNINGPATH']._serialized_end=2268 + _globals['_ADDPREREQUISITEREQUEST']._serialized_start=2270 + _globals['_ADDPREREQUISITEREQUEST']._serialized_end=2334 + _globals['_REMOVEPREREQUISITEREQUEST']._serialized_start=2336 + _globals['_REMOVEPREREQUISITEREQUEST']._serialized_end=2403 + _globals['_CREATEQUESTIONREQUEST']._serialized_start=2406 + _globals['_CREATEQUESTIONREQUEST']._serialized_end=2666 + _globals['_BATCHCREATEQUESTIONSREQUEST']._serialized_start=2668 + _globals['_BATCHCREATEQUESTIONSREQUEST']._serialized_end=2766 + _globals['_BATCHCREATEQUESTIONSRESPONSE']._serialized_start=2768 + _globals['_BATCHCREATEQUESTIONSRESPONSE']._serialized_end=2874 + _globals['_BATCHCREATEFAILURE']._serialized_start=2876 + _globals['_BATCHCREATEFAILURE']._serialized_end=2926 + _globals['_GETQUESTIONREQUEST']._serialized_start=2928 + _globals['_GETQUESTIONREQUEST']._serialized_end=2960 + _globals['_LISTQUESTIONSREQUEST']._serialized_start=2963 + _globals['_LISTQUESTIONSREQUEST']._serialized_end=3102 + _globals['_LISTQUESTIONSRESPONSE']._serialized_start=3104 + _globals['_LISTQUESTIONSRESPONSE']._serialized_end=3208 + _globals['_UPDATEQUESTIONREQUEST']._serialized_start=3211 + _globals['_UPDATEQUESTIONREQUEST']._serialized_end=3468 + _globals['_DELETEQUESTIONREQUEST']._serialized_start=3470 + _globals['_DELETEQUESTIONREQUEST']._serialized_end=3505 + _globals['_PUBLISHQUESTIONREQUEST']._serialized_start=3507 + _globals['_PUBLISHQUESTIONREQUEST']._serialized_end=3543 + _globals['_SEARCHQUESTIONSREQUEST']._serialized_start=3546 + _globals['_SEARCHQUESTIONSREQUEST']._serialized_end=3682 + _globals['_SEARCHQUESTIONSRESPONSE']._serialized_start=3684 + _globals['_SEARCHQUESTIONSRESPONSE']._serialized_end=3805 + _globals['_TEXTBOOKSERVICE']._serialized_start=3808 + _globals['_TEXTBOOKSERVICE']._serialized_end=4352 + _globals['_CHAPTERSERVICE']._serialized_start=4355 + _globals['_CHAPTERSERVICE']._serialized_end=4884 + _globals['_KNOWLEDGEGRAPHSERVICE']._serialized_start=4887 + _globals['_KNOWLEDGEGRAPHSERVICE']._serialized_end=5359 + _globals['_QUESTIONSERVICE']._serialized_start=5362 + _globals['_QUESTIONSERVICE']._serialized_end=6270 +# @@protoc_insertion_point(module_scope) diff --git a/services/ai/src/ai/proto_gen/content_pb2_grpc.py b/services/ai/src/ai/proto_gen/content_pb2_grpc.py new file mode 100644 index 0000000..ae944d1 --- /dev/null +++ b/services/ai/src/ai/proto_gen/content_pb2_grpc.py @@ -0,0 +1,1087 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import content_pb2 as content__pb2 + +GRPC_GENERATED_VERSION = '1.82.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in content_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class TextbookServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.CreateTextbook = channel.unary_unary( + '/next_edu_cloud.content.v1.TextbookService/CreateTextbook', + request_serializer=content__pb2.CreateTextbookRequest.SerializeToString, + response_deserializer=content__pb2.Textbook.FromString, + _registered_method=True) + self.GetTextbook = channel.unary_unary( + '/next_edu_cloud.content.v1.TextbookService/GetTextbook', + request_serializer=content__pb2.GetTextbookRequest.SerializeToString, + response_deserializer=content__pb2.Textbook.FromString, + _registered_method=True) + self.ListTextbooks = channel.unary_unary( + '/next_edu_cloud.content.v1.TextbookService/ListTextbooks', + request_serializer=content__pb2.ListTextbooksRequest.SerializeToString, + response_deserializer=content__pb2.ListTextbooksResponse.FromString, + _registered_method=True) + self.UpdateTextbook = channel.unary_unary( + '/next_edu_cloud.content.v1.TextbookService/UpdateTextbook', + request_serializer=content__pb2.UpdateTextbookRequest.SerializeToString, + response_deserializer=content__pb2.Textbook.FromString, + _registered_method=True) + self.DeleteTextbook = channel.unary_unary( + '/next_edu_cloud.content.v1.TextbookService/DeleteTextbook', + request_serializer=content__pb2.DeleteTextbookRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + + +class TextbookServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def CreateTextbook(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetTextbook(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListTextbooks(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateTextbook(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteTextbook(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_TextbookServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'CreateTextbook': grpc.unary_unary_rpc_method_handler( + servicer.CreateTextbook, + request_deserializer=content__pb2.CreateTextbookRequest.FromString, + response_serializer=content__pb2.Textbook.SerializeToString, + ), + 'GetTextbook': grpc.unary_unary_rpc_method_handler( + servicer.GetTextbook, + request_deserializer=content__pb2.GetTextbookRequest.FromString, + response_serializer=content__pb2.Textbook.SerializeToString, + ), + 'ListTextbooks': grpc.unary_unary_rpc_method_handler( + servicer.ListTextbooks, + request_deserializer=content__pb2.ListTextbooksRequest.FromString, + response_serializer=content__pb2.ListTextbooksResponse.SerializeToString, + ), + 'UpdateTextbook': grpc.unary_unary_rpc_method_handler( + servicer.UpdateTextbook, + request_deserializer=content__pb2.UpdateTextbookRequest.FromString, + response_serializer=content__pb2.Textbook.SerializeToString, + ), + 'DeleteTextbook': grpc.unary_unary_rpc_method_handler( + servicer.DeleteTextbook, + request_deserializer=content__pb2.DeleteTextbookRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.TextbookService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.TextbookService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class TextbookService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def CreateTextbook(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.TextbookService/CreateTextbook', + content__pb2.CreateTextbookRequest.SerializeToString, + content__pb2.Textbook.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetTextbook(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.TextbookService/GetTextbook', + content__pb2.GetTextbookRequest.SerializeToString, + content__pb2.Textbook.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ListTextbooks(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.TextbookService/ListTextbooks', + content__pb2.ListTextbooksRequest.SerializeToString, + content__pb2.ListTextbooksResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateTextbook(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.TextbookService/UpdateTextbook', + content__pb2.UpdateTextbookRequest.SerializeToString, + content__pb2.Textbook.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DeleteTextbook(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.TextbookService/DeleteTextbook', + content__pb2.DeleteTextbookRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class ChapterServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.CreateChapter = channel.unary_unary( + '/next_edu_cloud.content.v1.ChapterService/CreateChapter', + request_serializer=content__pb2.CreateChapterRequest.SerializeToString, + response_deserializer=content__pb2.Chapter.FromString, + _registered_method=True) + self.GetChapter = channel.unary_unary( + '/next_edu_cloud.content.v1.ChapterService/GetChapter', + request_serializer=content__pb2.GetChapterRequest.SerializeToString, + response_deserializer=content__pb2.Chapter.FromString, + _registered_method=True) + self.ListChapters = channel.unary_unary( + '/next_edu_cloud.content.v1.ChapterService/ListChapters', + request_serializer=content__pb2.ListChaptersRequest.SerializeToString, + response_deserializer=content__pb2.ListChaptersResponse.FromString, + _registered_method=True) + self.UpdateChapter = channel.unary_unary( + '/next_edu_cloud.content.v1.ChapterService/UpdateChapter', + request_serializer=content__pb2.UpdateChapterRequest.SerializeToString, + response_deserializer=content__pb2.Chapter.FromString, + _registered_method=True) + self.DeleteChapter = channel.unary_unary( + '/next_edu_cloud.content.v1.ChapterService/DeleteChapter', + request_serializer=content__pb2.DeleteChapterRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + + +class ChapterServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def CreateChapter(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetChapter(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListChapters(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateChapter(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteChapter(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_ChapterServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'CreateChapter': grpc.unary_unary_rpc_method_handler( + servicer.CreateChapter, + request_deserializer=content__pb2.CreateChapterRequest.FromString, + response_serializer=content__pb2.Chapter.SerializeToString, + ), + 'GetChapter': grpc.unary_unary_rpc_method_handler( + servicer.GetChapter, + request_deserializer=content__pb2.GetChapterRequest.FromString, + response_serializer=content__pb2.Chapter.SerializeToString, + ), + 'ListChapters': grpc.unary_unary_rpc_method_handler( + servicer.ListChapters, + request_deserializer=content__pb2.ListChaptersRequest.FromString, + response_serializer=content__pb2.ListChaptersResponse.SerializeToString, + ), + 'UpdateChapter': grpc.unary_unary_rpc_method_handler( + servicer.UpdateChapter, + request_deserializer=content__pb2.UpdateChapterRequest.FromString, + response_serializer=content__pb2.Chapter.SerializeToString, + ), + 'DeleteChapter': grpc.unary_unary_rpc_method_handler( + servicer.DeleteChapter, + request_deserializer=content__pb2.DeleteChapterRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.ChapterService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.ChapterService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class ChapterService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def CreateChapter(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.ChapterService/CreateChapter', + content__pb2.CreateChapterRequest.SerializeToString, + content__pb2.Chapter.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetChapter(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.ChapterService/GetChapter', + content__pb2.GetChapterRequest.SerializeToString, + content__pb2.Chapter.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ListChapters(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.ChapterService/ListChapters', + content__pb2.ListChaptersRequest.SerializeToString, + content__pb2.ListChaptersResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateChapter(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.ChapterService/UpdateChapter', + content__pb2.UpdateChapterRequest.SerializeToString, + content__pb2.Chapter.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DeleteChapter(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.ChapterService/DeleteChapter', + content__pb2.DeleteChapterRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class KnowledgeGraphServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.GetPrerequisites = channel.unary_unary( + '/next_edu_cloud.content.v1.KnowledgeGraphService/GetPrerequisites', + request_serializer=content__pb2.GetPrerequisitesRequest.SerializeToString, + response_deserializer=content__pb2.KnowledgePointsResponse.FromString, + _registered_method=True) + self.GetLearningPath = channel.unary_unary( + '/next_edu_cloud.content.v1.KnowledgeGraphService/GetLearningPath', + request_serializer=content__pb2.GetLearningPathRequest.SerializeToString, + response_deserializer=content__pb2.LearningPath.FromString, + _registered_method=True) + self.AddPrerequisite = channel.unary_unary( + '/next_edu_cloud.content.v1.KnowledgeGraphService/AddPrerequisite', + request_serializer=content__pb2.AddPrerequisiteRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + self.RemovePrerequisite = channel.unary_unary( + '/next_edu_cloud.content.v1.KnowledgeGraphService/RemovePrerequisite', + request_serializer=content__pb2.RemovePrerequisiteRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + + +class KnowledgeGraphServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def GetPrerequisites(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetLearningPath(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def AddPrerequisite(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RemovePrerequisite(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_KnowledgeGraphServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'GetPrerequisites': grpc.unary_unary_rpc_method_handler( + servicer.GetPrerequisites, + request_deserializer=content__pb2.GetPrerequisitesRequest.FromString, + response_serializer=content__pb2.KnowledgePointsResponse.SerializeToString, + ), + 'GetLearningPath': grpc.unary_unary_rpc_method_handler( + servicer.GetLearningPath, + request_deserializer=content__pb2.GetLearningPathRequest.FromString, + response_serializer=content__pb2.LearningPath.SerializeToString, + ), + 'AddPrerequisite': grpc.unary_unary_rpc_method_handler( + servicer.AddPrerequisite, + request_deserializer=content__pb2.AddPrerequisiteRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + 'RemovePrerequisite': grpc.unary_unary_rpc_method_handler( + servicer.RemovePrerequisite, + request_deserializer=content__pb2.RemovePrerequisiteRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.KnowledgeGraphService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.KnowledgeGraphService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class KnowledgeGraphService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def GetPrerequisites(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.KnowledgeGraphService/GetPrerequisites', + content__pb2.GetPrerequisitesRequest.SerializeToString, + content__pb2.KnowledgePointsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLearningPath(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.KnowledgeGraphService/GetLearningPath', + content__pb2.GetLearningPathRequest.SerializeToString, + content__pb2.LearningPath.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def AddPrerequisite(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.KnowledgeGraphService/AddPrerequisite', + content__pb2.AddPrerequisiteRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RemovePrerequisite(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.KnowledgeGraphService/RemovePrerequisite', + content__pb2.RemovePrerequisiteRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class QuestionServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.CreateQuestion = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/CreateQuestion', + request_serializer=content__pb2.CreateQuestionRequest.SerializeToString, + response_deserializer=content__pb2.Question.FromString, + _registered_method=True) + self.BatchCreateQuestions = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/BatchCreateQuestions', + request_serializer=content__pb2.BatchCreateQuestionsRequest.SerializeToString, + response_deserializer=content__pb2.BatchCreateQuestionsResponse.FromString, + _registered_method=True) + self.GetQuestion = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/GetQuestion', + request_serializer=content__pb2.GetQuestionRequest.SerializeToString, + response_deserializer=content__pb2.Question.FromString, + _registered_method=True) + self.ListQuestions = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/ListQuestions', + request_serializer=content__pb2.ListQuestionsRequest.SerializeToString, + response_deserializer=content__pb2.ListQuestionsResponse.FromString, + _registered_method=True) + self.UpdateQuestion = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/UpdateQuestion', + request_serializer=content__pb2.UpdateQuestionRequest.SerializeToString, + response_deserializer=content__pb2.Question.FromString, + _registered_method=True) + self.DeleteQuestion = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/DeleteQuestion', + request_serializer=content__pb2.DeleteQuestionRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + self.PublishQuestion = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/PublishQuestion', + request_serializer=content__pb2.PublishQuestionRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + self.SearchQuestions = channel.unary_unary( + '/next_edu_cloud.content.v1.QuestionService/SearchQuestions', + request_serializer=content__pb2.SearchQuestionsRequest.SerializeToString, + response_deserializer=content__pb2.SearchQuestionsResponse.FromString, + _registered_method=True) + + +class QuestionServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def CreateQuestion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def BatchCreateQuestions(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetQuestion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ListQuestions(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateQuestion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteQuestion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def PublishQuestion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SearchQuestions(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_QuestionServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'CreateQuestion': grpc.unary_unary_rpc_method_handler( + servicer.CreateQuestion, + request_deserializer=content__pb2.CreateQuestionRequest.FromString, + response_serializer=content__pb2.Question.SerializeToString, + ), + 'BatchCreateQuestions': grpc.unary_unary_rpc_method_handler( + servicer.BatchCreateQuestions, + request_deserializer=content__pb2.BatchCreateQuestionsRequest.FromString, + response_serializer=content__pb2.BatchCreateQuestionsResponse.SerializeToString, + ), + 'GetQuestion': grpc.unary_unary_rpc_method_handler( + servicer.GetQuestion, + request_deserializer=content__pb2.GetQuestionRequest.FromString, + response_serializer=content__pb2.Question.SerializeToString, + ), + 'ListQuestions': grpc.unary_unary_rpc_method_handler( + servicer.ListQuestions, + request_deserializer=content__pb2.ListQuestionsRequest.FromString, + response_serializer=content__pb2.ListQuestionsResponse.SerializeToString, + ), + 'UpdateQuestion': grpc.unary_unary_rpc_method_handler( + servicer.UpdateQuestion, + request_deserializer=content__pb2.UpdateQuestionRequest.FromString, + response_serializer=content__pb2.Question.SerializeToString, + ), + 'DeleteQuestion': grpc.unary_unary_rpc_method_handler( + servicer.DeleteQuestion, + request_deserializer=content__pb2.DeleteQuestionRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + 'PublishQuestion': grpc.unary_unary_rpc_method_handler( + servicer.PublishQuestion, + request_deserializer=content__pb2.PublishQuestionRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + 'SearchQuestions': grpc.unary_unary_rpc_method_handler( + servicer.SearchQuestions, + request_deserializer=content__pb2.SearchQuestionsRequest.FromString, + response_serializer=content__pb2.SearchQuestionsResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.QuestionService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.QuestionService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class QuestionService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def CreateQuestion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/CreateQuestion', + content__pb2.CreateQuestionRequest.SerializeToString, + content__pb2.Question.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def BatchCreateQuestions(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/BatchCreateQuestions', + content__pb2.BatchCreateQuestionsRequest.SerializeToString, + content__pb2.BatchCreateQuestionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetQuestion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/GetQuestion', + content__pb2.GetQuestionRequest.SerializeToString, + content__pb2.Question.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ListQuestions(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/ListQuestions', + content__pb2.ListQuestionsRequest.SerializeToString, + content__pb2.ListQuestionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateQuestion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/UpdateQuestion', + content__pb2.UpdateQuestionRequest.SerializeToString, + content__pb2.Question.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DeleteQuestion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/DeleteQuestion', + content__pb2.DeleteQuestionRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def PublishQuestion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/PublishQuestion', + content__pb2.PublishQuestionRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def SearchQuestions(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.content.v1.QuestionService/SearchQuestions', + content__pb2.SearchQuestionsRequest.SerializeToString, + content__pb2.SearchQuestionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/services/ai/src/ai/proto_gen/iam_pb2.py b/services/ai/src/ai/proto_gen/iam_pb2.py new file mode 100644 index 0000000..dd7aa7e --- /dev/null +++ b/services/ai/src/ai/proto_gen/iam_pb2.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: iam.proto +# Protobuf Python Version: 7.35.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 7, + 35, + 0, + '', + 'iam.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tiam.proto\x12\x15next_edu_cloud.iam.v1\"@\n\x0fRegisterRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\"/\n\x0cLoginRequest\x12\r\n\x05\x65mail\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\",\n\x13RefreshTokenRequest\x12\x15\n\rrefresh_token\x18\x01 \x01(\t\"7\n\rLogoutRequest\x12\x15\n\rrefresh_token\x18\x01 \x01(\t\x12\x0f\n\x07user_id\x18\x02 \x01(\t\"!\n\x0eLogoutResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"o\n\x0c\x41uthResponse\x12-\n\x04user\x18\x01 \x01(\x0b\x32\x1f.next_edu_cloud.iam.v1.UserInfo\x12\x30\n\x06tokens\x18\x02 \x01(\x0b\x32 .next_edu_cloud.iam.v1.TokenPair\"L\n\tTokenPair\x12\x14\n\x0c\x61\x63\x63\x65ss_token\x18\x01 \x01(\t\x12\x15\n\rrefresh_token\x18\x02 \x01(\t\x12\x12\n\nexpires_in\x18\x03 \x01(\x05\"%\n\x12GetUserInfoRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"(\n\x15GetUserProfileRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"D\n\x14UpdateProfileRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\r\n\x05\x65mail\x18\x03 \x01(\t\"X\n\x15\x43hangePasswordRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x18\n\x10\x63urrent_password\x18\x02 \x01(\t\x12\x14\n\x0cnew_password\x18\x03 \x01(\t\")\n\x16\x43hangePasswordResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\"(\n\x14\x42\x61tchGetUsersRequest\x12\x10\n\x08user_ids\x18\x01 \x03(\t\"G\n\x15\x42\x61tchGetUsersResponse\x12.\n\x05users\x18\x01 \x03(\x0b\x32\x1f.next_edu_cloud.iam.v1.UserInfo\"{\n\x08UserInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05\x65mail\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\r\n\x05roles\x18\x04 \x03(\t\x12\x13\n\x0bpermissions\x18\x05 \x03(\t\x12\x12\n\ndata_scope\x18\x06 \x01(\t\x12\x0e\n\x06status\x18\x07 \x01(\t\"1\n\x1eGetEffectivePermissionsRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"3\n\x1c\x45\x66\x66\x65\x63tivePermissionsResponse\x12\x13\n\x0bpermissions\x18\x01 \x03(\t\"@\n\x19GetEffectiveAccessRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x12\n\npermission\x18\x02 \x01(\t\">\n\x17\x45\x66\x66\x65\x63tiveAccessResponse\x12\x0f\n\x07\x61llowed\x18\x01 \x01(\x08\x12\x12\n\ndata_scope\x18\x02 \x01(\t\"/\n\x1cGetEffectiveDataScopeRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"\'\n\x11\x44\x61taScopeResponse\x12\x12\n\ndata_scope\x18\x01 \x01(\t\"&\n\x13GetViewportsRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"K\n\x11ViewportsResponse\x12\x36\n\tviewports\x18\x01 \x03(\x0b\x32#.next_edu_cloud.iam.v1.ViewportItem\"\xa1\x01\n\x0cViewportItem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\r\n\x05route\x18\x03 \x01(\t\x12\x0c\n\x04icon\x18\x04 \x01(\t\x12\x12\n\nsort_order\x18\x05 \x01(\t\x12\x1b\n\x13required_permission\x18\x06 \x01(\t\x12\r\n\x05level\x18\x07 \x01(\t\x12\x18\n\x10\x63omponent_config\x18\x08 \x01(\t\"\x15\n\x13GetPublicKeyRequest\"E\n\x11PublicKeyResponse\x12\x0b\n\x03kid\x18\x01 \x01(\t\x12\x0b\n\x03\x61lg\x18\x02 \x01(\t\x12\x16\n\x0epublic_key_pem\x18\x03 \x01(\t\"/\n\x1aGetChildrenByParentRequest\x12\x11\n\tparent_id\x18\x01 \x01(\t\"F\n\x10\x43hildrenResponse\x12\x32\n\x08\x63hildren\x18\x01 \x03(\x0b\x32 .next_edu_cloud.iam.v1.ChildInfo\"?\n\tChildInfo\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x10\n\x08relation\x18\x03 \x01(\t\"Z\n\x12\x45\x66\x66\x65\x63tiveDataScope\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\r\n\x05level\x18\x02 \x01(\t\x12\x11\n\tscope_ids\x18\x03 \x03(\t\x12\x11\n\tschool_id\x18\x04 \x01(\t2\x9b\x0c\n\nIamService\x12W\n\x08Register\x12&.next_edu_cloud.iam.v1.RegisterRequest\x1a#.next_edu_cloud.iam.v1.AuthResponse\x12Q\n\x05Login\x12#.next_edu_cloud.iam.v1.LoginRequest\x1a#.next_edu_cloud.iam.v1.AuthResponse\x12\\\n\x0cRefreshToken\x12*.next_edu_cloud.iam.v1.RefreshTokenRequest\x1a .next_edu_cloud.iam.v1.TokenPair\x12U\n\x06Logout\x12$.next_edu_cloud.iam.v1.LogoutRequest\x1a%.next_edu_cloud.iam.v1.LogoutResponse\x12Y\n\x0bGetUserInfo\x12).next_edu_cloud.iam.v1.GetUserInfoRequest\x1a\x1f.next_edu_cloud.iam.v1.UserInfo\x12_\n\x0eGetUserProfile\x12,.next_edu_cloud.iam.v1.GetUserProfileRequest\x1a\x1f.next_edu_cloud.iam.v1.UserInfo\x12]\n\rUpdateProfile\x12+.next_edu_cloud.iam.v1.UpdateProfileRequest\x1a\x1f.next_edu_cloud.iam.v1.UserInfo\x12m\n\x0e\x43hangePassword\x12,.next_edu_cloud.iam.v1.ChangePasswordRequest\x1a-.next_edu_cloud.iam.v1.ChangePasswordResponse\x12j\n\rBatchGetUsers\x12+.next_edu_cloud.iam.v1.BatchGetUsersRequest\x1a,.next_edu_cloud.iam.v1.BatchGetUsersResponse\x12\x85\x01\n\x17GetEffectivePermissions\x12\x35.next_edu_cloud.iam.v1.GetEffectivePermissionsRequest\x1a\x33.next_edu_cloud.iam.v1.EffectivePermissionsResponse\x12v\n\x12GetEffectiveAccess\x12\x30.next_edu_cloud.iam.v1.GetEffectiveAccessRequest\x1a..next_edu_cloud.iam.v1.EffectiveAccessResponse\x12w\n\x15GetEffectiveDataScope\x12\x33.next_edu_cloud.iam.v1.GetEffectiveDataScopeRequest\x1a).next_edu_cloud.iam.v1.EffectiveDataScope\x12\x64\n\x0cGetViewports\x12*.next_edu_cloud.iam.v1.GetViewportsRequest\x1a(.next_edu_cloud.iam.v1.ViewportsResponse\x12\x64\n\x0cGetPublicKey\x12*.next_edu_cloud.iam.v1.GetPublicKeyRequest\x1a(.next_edu_cloud.iam.v1.PublicKeyResponse\x12q\n\x13GetChildrenByParent\x12\x31.next_edu_cloud.iam.v1.GetChildrenByParentRequest\x1a\'.next_edu_cloud.iam.v1.ChildrenResponseb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'iam_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_REGISTERREQUEST']._serialized_start=36 + _globals['_REGISTERREQUEST']._serialized_end=100 + _globals['_LOGINREQUEST']._serialized_start=102 + _globals['_LOGINREQUEST']._serialized_end=149 + _globals['_REFRESHTOKENREQUEST']._serialized_start=151 + _globals['_REFRESHTOKENREQUEST']._serialized_end=195 + _globals['_LOGOUTREQUEST']._serialized_start=197 + _globals['_LOGOUTREQUEST']._serialized_end=252 + _globals['_LOGOUTRESPONSE']._serialized_start=254 + _globals['_LOGOUTRESPONSE']._serialized_end=287 + _globals['_AUTHRESPONSE']._serialized_start=289 + _globals['_AUTHRESPONSE']._serialized_end=400 + _globals['_TOKENPAIR']._serialized_start=402 + _globals['_TOKENPAIR']._serialized_end=478 + _globals['_GETUSERINFOREQUEST']._serialized_start=480 + _globals['_GETUSERINFOREQUEST']._serialized_end=517 + _globals['_GETUSERPROFILEREQUEST']._serialized_start=519 + _globals['_GETUSERPROFILEREQUEST']._serialized_end=559 + _globals['_UPDATEPROFILEREQUEST']._serialized_start=561 + _globals['_UPDATEPROFILEREQUEST']._serialized_end=629 + _globals['_CHANGEPASSWORDREQUEST']._serialized_start=631 + _globals['_CHANGEPASSWORDREQUEST']._serialized_end=719 + _globals['_CHANGEPASSWORDRESPONSE']._serialized_start=721 + _globals['_CHANGEPASSWORDRESPONSE']._serialized_end=762 + _globals['_BATCHGETUSERSREQUEST']._serialized_start=764 + _globals['_BATCHGETUSERSREQUEST']._serialized_end=804 + _globals['_BATCHGETUSERSRESPONSE']._serialized_start=806 + _globals['_BATCHGETUSERSRESPONSE']._serialized_end=877 + _globals['_USERINFO']._serialized_start=879 + _globals['_USERINFO']._serialized_end=1002 + _globals['_GETEFFECTIVEPERMISSIONSREQUEST']._serialized_start=1004 + _globals['_GETEFFECTIVEPERMISSIONSREQUEST']._serialized_end=1053 + _globals['_EFFECTIVEPERMISSIONSRESPONSE']._serialized_start=1055 + _globals['_EFFECTIVEPERMISSIONSRESPONSE']._serialized_end=1106 + _globals['_GETEFFECTIVEACCESSREQUEST']._serialized_start=1108 + _globals['_GETEFFECTIVEACCESSREQUEST']._serialized_end=1172 + _globals['_EFFECTIVEACCESSRESPONSE']._serialized_start=1174 + _globals['_EFFECTIVEACCESSRESPONSE']._serialized_end=1236 + _globals['_GETEFFECTIVEDATASCOPEREQUEST']._serialized_start=1238 + _globals['_GETEFFECTIVEDATASCOPEREQUEST']._serialized_end=1285 + _globals['_DATASCOPERESPONSE']._serialized_start=1287 + _globals['_DATASCOPERESPONSE']._serialized_end=1326 + _globals['_GETVIEWPORTSREQUEST']._serialized_start=1328 + _globals['_GETVIEWPORTSREQUEST']._serialized_end=1366 + _globals['_VIEWPORTSRESPONSE']._serialized_start=1368 + _globals['_VIEWPORTSRESPONSE']._serialized_end=1443 + _globals['_VIEWPORTITEM']._serialized_start=1446 + _globals['_VIEWPORTITEM']._serialized_end=1607 + _globals['_GETPUBLICKEYREQUEST']._serialized_start=1609 + _globals['_GETPUBLICKEYREQUEST']._serialized_end=1630 + _globals['_PUBLICKEYRESPONSE']._serialized_start=1632 + _globals['_PUBLICKEYRESPONSE']._serialized_end=1701 + _globals['_GETCHILDRENBYPARENTREQUEST']._serialized_start=1703 + _globals['_GETCHILDRENBYPARENTREQUEST']._serialized_end=1750 + _globals['_CHILDRENRESPONSE']._serialized_start=1752 + _globals['_CHILDRENRESPONSE']._serialized_end=1822 + _globals['_CHILDINFO']._serialized_start=1824 + _globals['_CHILDINFO']._serialized_end=1887 + _globals['_EFFECTIVEDATASCOPE']._serialized_start=1889 + _globals['_EFFECTIVEDATASCOPE']._serialized_end=1979 + _globals['_IAMSERVICE']._serialized_start=1982 + _globals['_IAMSERVICE']._serialized_end=3545 +# @@protoc_insertion_point(module_scope) diff --git a/services/ai/src/ai/proto_gen/iam_pb2_grpc.py b/services/ai/src/ai/proto_gen/iam_pb2_grpc.py new file mode 100644 index 0000000..ccaa252 --- /dev/null +++ b/services/ai/src/ai/proto_gen/iam_pb2_grpc.py @@ -0,0 +1,719 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc +import warnings + +import iam_pb2 as iam__pb2 + +GRPC_GENERATED_VERSION = '1.82.1' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + ' but the generated code in iam_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + + +class IamServiceStub: + """IamService 定义身份与访问管理契约 + 双入口策略(president §2.16):REST 供 gateway 透传 + admin-portal 直连, + gRPC 供 BFF 聚合调用。同一 Application Service 同时被两种 Controller 调用。 + gRPC 端口 50052,P2 即启用(I1 裁决)。 + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Register = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/Register', + request_serializer=iam__pb2.RegisterRequest.SerializeToString, + response_deserializer=iam__pb2.AuthResponse.FromString, + _registered_method=True) + self.Login = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/Login', + request_serializer=iam__pb2.LoginRequest.SerializeToString, + response_deserializer=iam__pb2.AuthResponse.FromString, + _registered_method=True) + self.RefreshToken = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/RefreshToken', + request_serializer=iam__pb2.RefreshTokenRequest.SerializeToString, + response_deserializer=iam__pb2.TokenPair.FromString, + _registered_method=True) + self.Logout = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/Logout', + request_serializer=iam__pb2.LogoutRequest.SerializeToString, + response_deserializer=iam__pb2.LogoutResponse.FromString, + _registered_method=True) + self.GetUserInfo = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetUserInfo', + request_serializer=iam__pb2.GetUserInfoRequest.SerializeToString, + response_deserializer=iam__pb2.UserInfo.FromString, + _registered_method=True) + self.GetUserProfile = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetUserProfile', + request_serializer=iam__pb2.GetUserProfileRequest.SerializeToString, + response_deserializer=iam__pb2.UserInfo.FromString, + _registered_method=True) + self.UpdateProfile = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/UpdateProfile', + request_serializer=iam__pb2.UpdateProfileRequest.SerializeToString, + response_deserializer=iam__pb2.UserInfo.FromString, + _registered_method=True) + self.ChangePassword = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/ChangePassword', + request_serializer=iam__pb2.ChangePasswordRequest.SerializeToString, + response_deserializer=iam__pb2.ChangePasswordResponse.FromString, + _registered_method=True) + self.BatchGetUsers = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/BatchGetUsers', + request_serializer=iam__pb2.BatchGetUsersRequest.SerializeToString, + response_deserializer=iam__pb2.BatchGetUsersResponse.FromString, + _registered_method=True) + self.GetEffectivePermissions = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetEffectivePermissions', + request_serializer=iam__pb2.GetEffectivePermissionsRequest.SerializeToString, + response_deserializer=iam__pb2.EffectivePermissionsResponse.FromString, + _registered_method=True) + self.GetEffectiveAccess = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetEffectiveAccess', + request_serializer=iam__pb2.GetEffectiveAccessRequest.SerializeToString, + response_deserializer=iam__pb2.EffectiveAccessResponse.FromString, + _registered_method=True) + self.GetEffectiveDataScope = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetEffectiveDataScope', + request_serializer=iam__pb2.GetEffectiveDataScopeRequest.SerializeToString, + response_deserializer=iam__pb2.EffectiveDataScope.FromString, + _registered_method=True) + self.GetViewports = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetViewports', + request_serializer=iam__pb2.GetViewportsRequest.SerializeToString, + response_deserializer=iam__pb2.ViewportsResponse.FromString, + _registered_method=True) + self.GetPublicKey = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetPublicKey', + request_serializer=iam__pb2.GetPublicKeyRequest.SerializeToString, + response_deserializer=iam__pb2.PublicKeyResponse.FromString, + _registered_method=True) + self.GetChildrenByParent = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/GetChildrenByParent', + request_serializer=iam__pb2.GetChildrenByParentRequest.SerializeToString, + response_deserializer=iam__pb2.ChildrenResponse.FromString, + _registered_method=True) + + +class IamServiceServicer: + """IamService 定义身份与访问管理契约 + 双入口策略(president §2.16):REST 供 gateway 透传 + admin-portal 直连, + gRPC 供 BFF 聚合调用。同一 Application Service 同时被两种 Controller 调用。 + gRPC 端口 50052,P2 即启用(I1 裁决)。 + """ + + def Register(self, request, context): + """认证类 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Login(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RefreshToken(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Logout(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetUserInfo(self, request, context): + """用户信息类 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetUserProfile(self, request, context): + """GetUserProfile 是 GetUserInfo 的语义别名(student-bff 期望的命名). + 返回结构与 GetUserInfo 完全相同,仅 RPC 名不同以兼容下游契约. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateProfile(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ChangePassword(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def BatchGetUsers(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEffectivePermissions(self, request, context): + """权限与视口类 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEffectiveAccess(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetEffectiveDataScope(self, request, context): + """GetEffectiveDataScope 解析用户可见数据范围(DataScope 6 级). + data-ana gRPC 调用此 RPC 解析查询过滤范围(coord-cross-review §2 #3 裁决 P4 补全). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetViewports(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetPublicKey(self, request, context): + """密钥与关系类 + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetChildrenByParent(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_IamServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Register': grpc.unary_unary_rpc_method_handler( + servicer.Register, + request_deserializer=iam__pb2.RegisterRequest.FromString, + response_serializer=iam__pb2.AuthResponse.SerializeToString, + ), + 'Login': grpc.unary_unary_rpc_method_handler( + servicer.Login, + request_deserializer=iam__pb2.LoginRequest.FromString, + response_serializer=iam__pb2.AuthResponse.SerializeToString, + ), + 'RefreshToken': grpc.unary_unary_rpc_method_handler( + servicer.RefreshToken, + request_deserializer=iam__pb2.RefreshTokenRequest.FromString, + response_serializer=iam__pb2.TokenPair.SerializeToString, + ), + 'Logout': grpc.unary_unary_rpc_method_handler( + servicer.Logout, + request_deserializer=iam__pb2.LogoutRequest.FromString, + response_serializer=iam__pb2.LogoutResponse.SerializeToString, + ), + 'GetUserInfo': grpc.unary_unary_rpc_method_handler( + servicer.GetUserInfo, + request_deserializer=iam__pb2.GetUserInfoRequest.FromString, + response_serializer=iam__pb2.UserInfo.SerializeToString, + ), + 'GetUserProfile': grpc.unary_unary_rpc_method_handler( + servicer.GetUserProfile, + request_deserializer=iam__pb2.GetUserProfileRequest.FromString, + response_serializer=iam__pb2.UserInfo.SerializeToString, + ), + 'UpdateProfile': grpc.unary_unary_rpc_method_handler( + servicer.UpdateProfile, + request_deserializer=iam__pb2.UpdateProfileRequest.FromString, + response_serializer=iam__pb2.UserInfo.SerializeToString, + ), + 'ChangePassword': grpc.unary_unary_rpc_method_handler( + servicer.ChangePassword, + request_deserializer=iam__pb2.ChangePasswordRequest.FromString, + response_serializer=iam__pb2.ChangePasswordResponse.SerializeToString, + ), + 'BatchGetUsers': grpc.unary_unary_rpc_method_handler( + servicer.BatchGetUsers, + request_deserializer=iam__pb2.BatchGetUsersRequest.FromString, + response_serializer=iam__pb2.BatchGetUsersResponse.SerializeToString, + ), + 'GetEffectivePermissions': grpc.unary_unary_rpc_method_handler( + servicer.GetEffectivePermissions, + request_deserializer=iam__pb2.GetEffectivePermissionsRequest.FromString, + response_serializer=iam__pb2.EffectivePermissionsResponse.SerializeToString, + ), + 'GetEffectiveAccess': grpc.unary_unary_rpc_method_handler( + servicer.GetEffectiveAccess, + request_deserializer=iam__pb2.GetEffectiveAccessRequest.FromString, + response_serializer=iam__pb2.EffectiveAccessResponse.SerializeToString, + ), + 'GetEffectiveDataScope': grpc.unary_unary_rpc_method_handler( + servicer.GetEffectiveDataScope, + request_deserializer=iam__pb2.GetEffectiveDataScopeRequest.FromString, + response_serializer=iam__pb2.EffectiveDataScope.SerializeToString, + ), + 'GetViewports': grpc.unary_unary_rpc_method_handler( + servicer.GetViewports, + request_deserializer=iam__pb2.GetViewportsRequest.FromString, + response_serializer=iam__pb2.ViewportsResponse.SerializeToString, + ), + 'GetPublicKey': grpc.unary_unary_rpc_method_handler( + servicer.GetPublicKey, + request_deserializer=iam__pb2.GetPublicKeyRequest.FromString, + response_serializer=iam__pb2.PublicKeyResponse.SerializeToString, + ), + 'GetChildrenByParent': grpc.unary_unary_rpc_method_handler( + servicer.GetChildrenByParent, + request_deserializer=iam__pb2.GetChildrenByParentRequest.FromString, + response_serializer=iam__pb2.ChildrenResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.iam.v1.IamService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.iam.v1.IamService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class IamService: + """IamService 定义身份与访问管理契约 + 双入口策略(president §2.16):REST 供 gateway 透传 + admin-portal 直连, + gRPC 供 BFF 聚合调用。同一 Application Service 同时被两种 Controller 调用。 + gRPC 端口 50052,P2 即启用(I1 裁决)。 + """ + + @staticmethod + def Register(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/Register', + iam__pb2.RegisterRequest.SerializeToString, + iam__pb2.AuthResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Login(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/Login', + iam__pb2.LoginRequest.SerializeToString, + iam__pb2.AuthResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def RefreshToken(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/RefreshToken', + iam__pb2.RefreshTokenRequest.SerializeToString, + iam__pb2.TokenPair.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Logout(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/Logout', + iam__pb2.LogoutRequest.SerializeToString, + iam__pb2.LogoutResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetUserInfo(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetUserInfo', + iam__pb2.GetUserInfoRequest.SerializeToString, + iam__pb2.UserInfo.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetUserProfile(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetUserProfile', + iam__pb2.GetUserProfileRequest.SerializeToString, + iam__pb2.UserInfo.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateProfile(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/UpdateProfile', + iam__pb2.UpdateProfileRequest.SerializeToString, + iam__pb2.UserInfo.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ChangePassword(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/ChangePassword', + iam__pb2.ChangePasswordRequest.SerializeToString, + iam__pb2.ChangePasswordResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def BatchGetUsers(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/BatchGetUsers', + iam__pb2.BatchGetUsersRequest.SerializeToString, + iam__pb2.BatchGetUsersResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetEffectivePermissions(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetEffectivePermissions', + iam__pb2.GetEffectivePermissionsRequest.SerializeToString, + iam__pb2.EffectivePermissionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetEffectiveAccess(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetEffectiveAccess', + iam__pb2.GetEffectiveAccessRequest.SerializeToString, + iam__pb2.EffectiveAccessResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetEffectiveDataScope(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetEffectiveDataScope', + iam__pb2.GetEffectiveDataScopeRequest.SerializeToString, + iam__pb2.EffectiveDataScope.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetViewports(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetViewports', + iam__pb2.GetViewportsRequest.SerializeToString, + iam__pb2.ViewportsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetPublicKey(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetPublicKey', + iam__pb2.GetPublicKeyRequest.SerializeToString, + iam__pb2.PublicKeyResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetChildrenByParent(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/next_edu_cloud.iam.v1.IamService/GetChildrenByParent', + iam__pb2.GetChildrenByParentRequest.SerializeToString, + iam__pb2.ChildrenResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/services/ai/tests/test_clients.py b/services/ai/tests/test_clients.py index 98e5b7d..2c2ec43 100644 --- a/services/ai/tests/test_clients.py +++ b/services/ai/tests/test_clients.py @@ -19,7 +19,7 @@ class TestContentClientMock: """查询学习路径.""" client = ContentClientMock() result = await client.get_learning_path("s-1", "math") - assert len(result) == 3 + assert len(result) == 2 async def test_create_questions(self) -> None: """批量创建题目.""" diff --git a/services/ai/tests/test_coverage_gaps.py b/services/ai/tests/test_coverage_gaps.py index cd0ea3f..d02b7b7 100644 --- a/services/ai/tests/test_coverage_gaps.py +++ b/services/ai/tests/test_coverage_gaps.py @@ -419,34 +419,55 @@ async def test_content_grpc_connect_close() -> None: async def test_content_grpc_methods_not_connected() -> None: + """未连接时调用方法应抛 AIError(不降级到 mock).""" client = ContentClientGrpc() assert client.is_available() is False - pre = await client.get_prerequisites("kp-1") - assert len(pre) == 1 - path = await client.get_learning_path("s-1", "math") - assert len(path) == 3 - questions = [ - QuestionInput( - question="q1", - answer="a1", - explanation="e1", - question_type="short_answer", - difficulty="easy", - knowledge_point_ids=["kp-1"], - ), - ] - result = await client.create_questions(questions, user_id="u-1") - assert len(result) == 1 + with pytest.raises(AIError, match="not connected"): + await client.get_prerequisites("kp-1") + with pytest.raises(AIError, match="not connected"): + await client.get_learning_path("s-1", "math") + with pytest.raises(AIError, match="not connected"): + await client.create_questions([], user_id="u-1") async def test_content_grpc_methods_connected() -> None: + """连接后通过 gRPC stub 发起真实调用(使用 mock stub 验证).""" + from src.ai.proto_gen import content_pb2 + client = ContentClientGrpc() client._channel = MagicMock() + # 构造 mock stub 返回 proto 响应 + mock_kg_stub = MagicMock() + mock_kg_stub.GetPrerequisites = AsyncMock( + return_value=content_pb2.KnowledgePointsResponse( + points=[ + content_pb2.KnowledgePoint(id="kp_pre", title="前置知识点"), + ], + ), + ) + mock_kg_stub.GetLearningPath = AsyncMock( + return_value=content_pb2.LearningPath( + points=[ + content_pb2.KnowledgePoint(id="kp_1", title="知识点1"), + content_pb2.KnowledgePoint(id="kp_2", title="知识点2"), + ], + ), + ) + mock_q_stub = MagicMock() + mock_q_stub.BatchCreateQuestions = AsyncMock( + return_value=content_pb2.BatchCreateQuestionsResponse(ids=["q_new_1"]), + ) + client._kg_stub = mock_kg_stub + client._q_stub = mock_q_stub assert client.is_available() is True + pre = await client.get_prerequisites("kp-1") assert len(pre) == 1 + assert pre[0].id == "kp_pre" + path = await client.get_learning_path("s-1", "math") - assert len(path) == 3 + assert len(path) == 2 + questions = [ QuestionInput( question="q1", @@ -459,13 +480,25 @@ async def test_content_grpc_methods_connected() -> None: ] result = await client.create_questions(questions, user_id="u-1") assert len(result) == 1 + assert result[0].id == "q_new_1" async def test_content_grpc_create_questions_exception_raises() -> None: + """gRPC 调用失败时包装为 AIError.""" + import grpc + client = ContentClientGrpc() client._channel = MagicMock() - client._mock = MagicMock() - client._mock.create_questions = AsyncMock(side_effect=RuntimeError("boom")) + mock_q_stub = MagicMock() + mock_q_stub.BatchCreateQuestions = AsyncMock( + side_effect=grpc.aio.AioRpcError( + code=grpc.StatusCode.UNAVAILABLE, + initial_metadata=[], + trailing_metadata=[], + details="service unavailable", + ), + ) + client._q_stub = mock_q_stub with pytest.raises(AIError) as exc_info: await client.create_questions([], user_id="u-1") assert exc_info.value.code == ErrorCode.AI_DOWNSTREAM_UNAVAILABLE @@ -490,29 +523,71 @@ async def test_data_ana_grpc_connect_close() -> None: async def test_data_ana_grpc_methods_not_connected() -> None: + """未连接时调用方法应抛 AIError.""" client = DataAnaClientGrpc() assert client.is_available() is False - perf = await client.get_class_performance("c-1", "math") - assert perf.class_id == "c-1" - assert perf.average_score > 0 - weak = await client.get_student_weakness("s-1", "math") - assert weak.student_id == "s-1" - assert len(weak.weak_points) > 0 - trend = await client.get_learning_trend("s-1") - assert trend.student_id == "s-1" - assert len(trend.points) > 0 + with pytest.raises(AIError, match="not connected"): + await client.get_class_performance("c-1", "math") + with pytest.raises(AIError, match="not connected"): + await client.get_student_weakness("s-1", "math") + with pytest.raises(AIError, match="not connected"): + await client.get_learning_trend("s-1") async def test_data_ana_grpc_methods_connected() -> None: + """连接后通过 gRPC stub 真实调用.""" + from src.ai.proto_gen import analytics_pb2 + client = DataAnaClientGrpc() client._channel = MagicMock() + mock_stub = MagicMock() + mock_stub.GetClassPerformance = AsyncMock( + return_value=analytics_pb2.ClassPerformance( + class_id="c-1", + average_score=82.5, + pass_rate=0.9, + scores=[ + analytics_pb2.StudentScore(student_id="s-1", score=85.0, grade="A"), + ], + ), + ) + mock_stub.GetStudentWeakness = AsyncMock( + return_value=analytics_pb2.StudentWeakness( + student_id="s-1", + weak_points=[ + analytics_pb2.WeakPoint( + knowledge_point_id="kp-1", + title="函数", + mastery=0.4, + ), + ], + ), + ) + mock_stub.GetLearningTrend = AsyncMock( + return_value=analytics_pb2.LearningTrend( + student_id="s-1", + points=[ + analytics_pb2.TrendPoint(date=20260101, score=70.0), + analytics_pb2.TrendPoint(date=20260201, score=75.0), + ], + ), + ) + client._stub = mock_stub assert client.is_available() is True + perf = await client.get_class_performance("c-1", "math") assert perf.class_id == "c-1" + assert perf.average_score == 82.5 + assert len(perf.scores) == 1 + weak = await client.get_student_weakness("s-1", "math") assert weak.student_id == "s-1" + assert len(weak.weak_points) == 1 + assert weak.weak_points[0].knowledge_point_id == "kp-1" + trend = await client.get_learning_trend("s-1") assert trend.student_id == "s-1" + assert len(trend.points) == 2 # --------------------------------------------------------------------------- @@ -534,16 +609,46 @@ async def test_iam_grpc_connect_close() -> None: async def test_iam_grpc_get_effective_data_scope() -> None: + """未连接时抛 AIError;连接后通过 gRPC stub 真实调用.""" + from src.ai.proto_gen import iam_pb2 + client = IamClientGrpc() assert client.is_available() is False + with pytest.raises(AIError, match="not connected"): + await client.get_effective_data_scope("u-1") + + # 连接后通过 mock stub 调用 + client._channel = MagicMock() + mock_stub = MagicMock() + mock_stub.GetEffectiveDataScope = AsyncMock( + return_value=iam_pb2.EffectiveDataScope( + user_id="u-1", + level="CLASS", + scope_ids=["class_001", "class_002"], + school_id="school_001", + ), + ) + client._stub = mock_stub + assert client.is_available() is True + scope = await client.get_effective_data_scope("u-1") assert scope.user_id == "u-1" - assert scope.school_id == "school_mock_001" - assert len(scope.class_ids) > 0 - client._channel = MagicMock() - assert client.is_available() is True - scope2 = await client.get_effective_data_scope("u-2") - assert scope2.user_id == "u-2" + assert scope.school_id == "school_001" + assert scope.class_ids == ["class_001", "class_002"] + assert scope.is_admin is False + + # ALL level 测试 + mock_stub.GetEffectiveDataScope = AsyncMock( + return_value=iam_pb2.EffectiveDataScope( + user_id="u-admin", + level="ALL", + scope_ids=[], + school_id="", + ), + ) + scope2 = await client.get_effective_data_scope("u-admin") + assert scope2.is_admin is True + assert scope2.role == "admin" # --------------------------------------------------------------------------- diff --git a/services/ai/tests/test_lesson_workflow.py b/services/ai/tests/test_lesson_workflow.py index bdb1aa2..8eae79c 100644 --- a/services/ai/tests/test_lesson_workflow.py +++ b/services/ai/tests/test_lesson_workflow.py @@ -23,13 +23,15 @@ from src.ai.workflow.state_store import WorkflowState, WorkflowStateStore from .conftest import MockProvider # 有效的 LLM JSON 输出(通过三道防线评估) -VALID_QUESTION_JSON = json.dumps({ - "question": "什么是函数?", - "answer": "函数是一种对应关系", - "explanation": "函数定义", - "difficulty": "medium", - "question_type": "short_answer", -}) +VALID_QUESTION_JSON = json.dumps( + { + "question": "什么是函数?", + "answer": "函数是一种对应关系", + "explanation": "函数定义", + "difficulty": "medium", + "question_type": "short_answer", + } +) def _make_chain(provider: MockProvider | None = None) -> ProviderFailoverChain: @@ -135,11 +137,16 @@ class TestLessonPlanWorkflowConfirm: store = WorkflowStateStore(redis=None) state = _make_state( status="pending_review", - questions=[GeneratedQuestionData( - question="q1", answer="a1", explanation="e1", - question_type="short_answer", difficulty="easy", - knowledge_point_ids=["kp_1"], - )], + questions=[ + GeneratedQuestionData( + question="q1", + answer="a1", + explanation="e1", + question_type="short_answer", + difficulty="easy", + knowledge_point_ids=["kp_1"], + ) + ], ) await store.create(state) svc = _make_service(store=store, content_client=ContentClientMock()) @@ -166,11 +173,16 @@ class TestLessonPlanWorkflowConfirm: store = WorkflowStateStore(redis=None) state = _make_state( status="pending_review", - questions=[GeneratedQuestionData( - question="original", answer="a1", explanation="e1", - question_type="short_answer", difficulty="easy", - knowledge_point_ids=["kp_1"], - )], + questions=[ + GeneratedQuestionData( + question="original", + answer="a1", + explanation="e1", + question_type="short_answer", + difficulty="easy", + knowledge_point_ids=["kp_1"], + ) + ], ) await store.create(state) @@ -200,7 +212,7 @@ class TestLessonPlanWorkflowSteps: analysis = await svc._step1_analyze(state) assert "class_performance" in analysis assert analysis["class_performance"]["average_score"] == 78.5 - assert analysis["class_performance"]["student_count"] == 3 + assert analysis["class_performance"]["student_count"] == 2 assert "weak_students" in analysis async def test_step1_analyze_no_client_degraded(self) -> None: @@ -214,13 +226,14 @@ class TestLessonPlanWorkflowSteps: svc = _make_service(content_client=ContentClientMock()) state = _make_state() kps = await svc._step2_recommend(state) - assert len(kps) == 3 + assert len(kps) == 2 assert kps[0]["id"] == "kp_001" async def test_step2_recommend_no_client_fallback(self) -> None: svc = _make_service(content_client=None) state = _make_state(topic="函数") kps = await svc._step2_recommend(state) + # 无 content_client 时降级到内置 3 个默认知识点 assert len(kps) == 3 assert "基础概念" in kps[0]["title"] assert "函数" in kps[0]["title"] @@ -233,7 +246,8 @@ class TestLessonPlanWorkflowSteps: ) state = _make_state(question_count=1, target_difficulty="medium") questions = await svc._step3_generate( - state, [{"id": "kp_1", "title": "KP1"}], + state, + [{"id": "kp_1", "title": "KP1"}], ) assert len(questions) == 1 assert questions[0].question == "什么是函数?" @@ -245,7 +259,8 @@ class TestLessonPlanWorkflowSteps: svc = _make_service(provider=provider) state = _make_state(question_count=1) questions = await svc._step3_generate( - state, [{"id": "kp_1", "title": "KP1"}], + state, + [{"id": "kp_1", "title": "KP1"}], ) assert len(questions) == 1 assert questions[0].degraded is True diff --git a/services/ai/tests/test_main_app.py b/services/ai/tests/test_main_app.py index 18fff38..d157053 100644 --- a/services/ai/tests/test_main_app.py +++ b/services/ai/tests/test_main_app.py @@ -37,17 +37,24 @@ from src.ai.middleware.error_handler import ( @pytest.fixture async def client() -> AsyncGenerator[httpx.AsyncClient, None]: - """HTTP client wired to the FastAPI app (dev_mode=True, permissions skipped).""" - from src.ai.main import _permission_guard, app + """HTTP client wired to the FastAPI app (dev_mode=True, permissions skipped). + + 注入 ContentClientMock 到 workflow_service,因为测试环境无真实 content gRPC server。 + """ + from src.ai.clients import ContentClientMock + from src.ai.main import _permission_guard, _workflow_service, app original = _permission_guard._dev_mode + original_content = _workflow_service._content_client # noqa: SLF001 _permission_guard._dev_mode = True + _workflow_service._content_client = ContentClientMock() # noqa: SLF001 try: transport = ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: yield c finally: _permission_guard._dev_mode = original + _workflow_service._content_client = original_content # noqa: SLF001 @pytest.fixture @@ -136,14 +143,14 @@ async def test_handle_unknown_error() -> None: async def test_grpc_error_mapper_ai_error() -> None: """grpc_error_mapper maps AIError to correct gRPC status code.""" cases = [ - (ErrorCode.AI_UNAUTHORIZED, 8), # UNAUTHENTICATED - (ErrorCode.AI_FORBIDDEN, 7), # PERMISSION_DENIED - (ErrorCode.AI_RATE_LIMITED, 9), # RESOURCE_EXHAUSTED - (ErrorCode.AI_QUOTA_EXCEEDED, 9), # RESOURCE_EXHAUSTED - (ErrorCode.AI_INVALID_MODEL, 3), # INVALID_ARGUMENT + (ErrorCode.AI_UNAUTHORIZED, 8), # UNAUTHENTICATED + (ErrorCode.AI_FORBIDDEN, 7), # PERMISSION_DENIED + (ErrorCode.AI_RATE_LIMITED, 9), # RESOURCE_EXHAUSTED + (ErrorCode.AI_QUOTA_EXCEEDED, 9), # RESOURCE_EXHAUSTED + (ErrorCode.AI_INVALID_MODEL, 3), # INVALID_ARGUMENT (ErrorCode.AI_WORKFLOW_NOT_FOUND, 5), # NOT_FOUND (ErrorCode.AI_WORKFLOW_STATE_INVALID, 10), # FAILED_PRECONDITION - (ErrorCode.AI_INTERNAL_ERROR, 13), # INTERNAL + (ErrorCode.AI_INTERNAL_ERROR, 13), # INTERNAL ] for code, expected_grpc_status in cases: exc = AIError(code, f"test {code.value}") @@ -390,9 +397,7 @@ async def test_confirm_lesson_plan_success(client: httpx.AsyncClient) -> None: break await asyncio.sleep(0.1) - assert status == "pending_review", ( - f"Workflow did not reach pending_review, got: {status}" - ) + assert status == "pending_review", f"Workflow did not reach pending_review, got: {status}" resp = await client.post(f"/v1/ai/lesson-plan/confirm/{workflow_id}") assert resp.status_code == 200