From aac26c7c6f7d25756945c97ce94916b8e65456c2 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:57:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20v2=20=E6=96=B0=E5=A2=9E=20GenerateR?= =?UTF-8?q?eport=20RPC=20+=20ReportService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第 9 个 RPC GenerateReport(学情报告生成):data-ana 学情数据 → LLM 生成 → 结构化提取 新增 ReportService 业务编排层 + GenerateReportRequest/GeneratedReport 模型 gRPC servicer + HTTP POST /v1/ai/generate/report(权限 ai:report:generate) proto_gen 重新生成 + 测试覆盖(servicer/service/HTTP/模型/权限 共 26 用例) 402 测试通过,覆盖率 88.5% --- services/ai/.coverage | Bin 53248 -> 53248 bytes services/ai/docs/nextstep-v2.md | 340 ++++++++++++ services/ai/src/ai/grpc_server/server.py | 2 + services/ai/src/ai/grpc_server/servicer.py | 52 +- services/ai/src/ai/main.py | 52 +- services/ai/src/ai/middleware/permission.py | 4 + services/ai/src/ai/models/__init__.py | 10 + services/ai/src/ai/models/report.py | 37 ++ services/ai/src/ai/proto_gen/ai_pb2.py | 10 +- services/ai/src/ai/proto_gen/ai_pb2_grpc.py | 46 +- services/ai/src/ai/proto_gen/content_pb2.py | 60 +- .../ai/src/ai/proto_gen/content_pb2_grpc.py | 517 ++++++++++++++++++ services/ai/src/ai/proto_gen/iam_pb2.py | 94 ++-- services/ai/src/ai/proto_gen/iam_pb2_grpc.py | 351 ++++++------ services/ai/src/ai/services/__init__.py | 3 + services/ai/src/ai/services/report_service.py | 219 ++++++++ services/ai/tests/test_grpc_servicer.py | 151 ++++- services/ai/tests/test_main_app.py | 69 +++ services/ai/tests/test_models.py | 45 +- services/ai/tests/test_permission.py | 10 + services/ai/tests/test_services.py | 160 +++++- 21 files changed, 1954 insertions(+), 278 deletions(-) create mode 100644 services/ai/docs/nextstep-v2.md create mode 100644 services/ai/src/ai/models/report.py create mode 100644 services/ai/src/ai/services/report_service.py diff --git a/services/ai/.coverage b/services/ai/.coverage index 3b0db72e8f99208200d4fa0a56d94eb59d1d37fb..59bd9cfff2d58f7b6aaa92601e827ae4cd44a1e8 100644 GIT binary patch delta 3084 zcmZWqc~nzZ8h>lH_wsHCAqfOX!jiCtJrJxKkxjtCk&X+a4HVH*7m!7(6oWd))8lfq zy{C0La}+zHW2;3$#%)~U*mK4?dPc0%B6BK>r*)|y=n+MUkh!@6GSl}*e&79l%e~)s ze=o1`0Nr?iZei@SjY@)IyP;@4@6X-l{>+td23fD{FS2r3g!G>Dh;*YgQ_4%OOZG_S zOEltk@m}!~aiFL}^ob~6M6+kum24!_$22qVGZy+W{S{qCM^JaEFQ`%~srBQtBo$+W z$1-A>G=|s=?-YeXYPLU_3J2zzV8-kq(h5svXOJmyUT!%2=WIVR8G;-WyfIe>g-ky- z2_*s8nh}DTw=>mDB273ku_sf@B+x|t_(B1@Geu-Ps%C zPqySr6*aY`wdIbQ+REy3OVtK@EY#DX(5vQRemK$Dm{A@J7fF z4cQ#*&Qg-m@JUu48-;v6Q+>)+t5!SSuUodlo0=6l*}+8W#cZC8!2Nrfk!%%dMm=XS zCKQPWRG;HdhU0;t0J+0av8JNd;XuW#Vem#yDjPbvqb1i!hM>rF#yHVgORknNyjh6g zP_B~EPbCuPD9K<{C>*xrieS|oo(zJeh2g4d?>JUftgfgnuST0$0%@Ym+m}#Qxn_CA z3Ph#rCgv^4Q*UExLbuz6|mO5>kn3gu|Kn+|*`MeNapc+*H7^DGHici;fcNQ@N~sRn3yu7laO! z3cN9GrwiuNP8*d>fOgqoS)^>a^uF|{v>tp0NHi;kr3QGcS|6)zPBi3&svdzM`#x+Q{a zlvFNhmwYTKl4!*@n0{j7Y5hYh37hA~vbLGbW^!EUS~hJqvssE)8M6bC>>WiJNb-e? zOi1i1Bzs|@d0gx&HaVL?V!u-q3*XN4W0I!RPMo+WQ$r@ggm=8UR*3N3B%nIxahU@q z{!OG;<{T3@K_Hof|CPA~1{a&XDp&tP<*@|}61n#colt6bmJb;NAO9wgjm}|^$fx*x zriwgj{NQggFJ^OWa-%w~+An72A>UyNhkC2DkKD^BPt?xoXVU zkbd~CT=_aA|6eaLdqpoPt*d1Mad?$u0VWj+vz29 zL!CC+E&{v+{op3}8Z?4spb%sOD+mKR!6OU^r-YTlyTUvnP0$M}K_Y)4e<<&f-<1DD zep3EN`A70HdA2-J9>5Rto%~7uQ+_93$1mk``00EEujgs*9(R>%s2#3+ICFFb&?h6_u7?|U=n|8>MS`g_#zbUf z#%}4mHga6Q=cgY$WB;xnbO(Jo`0cPh;`o8#{TH*M6Hv&a2zYRZc|klfW6WEgofsP- zd!OphwIA+DA94o7A=(;4E%NU@dwSn9;-~w|7BqWqxt7xy!W1BpqLnOXuRcJ7B<|e_$UHUme$P8nt2l<4dvuAMl zNgJmPL{wrJ)t~p=MQDj_eRcLnhk6$heH_pskHP1Nyrq3cThn4IEek+|T5;sW`sYdT z_AaYa=I152bY=@7k*bibQg2z>QLphuX1pJD>Ex?dZk?!)_;9)>7*x3zs+GuLQEllu z(J=U7Pkm0eqS5W~tY8#Ah!63lO0G4mzpO7E{8=nkAV#62&d&i@kv57r@0l)Ph{vfU z@pyRp%S~xbZs+|%(S($Do6G;BcaQEJeKmM>cv?^Ua754GwPANp+vp{1QDEYY7jwPe(D)I(J@kAA;3xU=r%}HVYfU4B@t5{$4mC92Kq$SAd2i15Sct;4nA<;z6YFLU}CIMXB z6k=DaFz4rsL8Zjt>w`h5z+eLy5(Nwiat!f2hB$bxNf#C<#tWSYLja3G%V6-QF=*OG Inndyc1+eC?iU0rr delta 2790 zcmZ9Mdr(u^9mnr8H_6StCnO=k1PBBYUg3?&iV7$oC{VU_r{V(xRkWaT`}>{W zd41<@+|O>@&mQG+ST}PR3po*`Z)^18EpfLvPc*6Ts`scp>M+%P)iKpNRj!Ixb}4_O z{E1Q_ToJG^SCABcQ*2UX@ixk6BFb+xCiYH8(?`kFe(%$$q|OB}c~SB0<7H1HW5)hyuE z`O#2o=F(ZJabh6gvRni^FO-JL!1({qFOL@t@Itp~O4H^}2k*|(p z!&!yFczdA}`wDfq2Y9|vM~B09rqf|qp6#YXab>9;ub8dnEG)Go&_!HQvuauOa=5Te zGn~qfOXn!@KW5APB-j8?)(I%aIeIRHrJ5!L6K{ev`(t|7935w5sfO2r(&MRKRbB7# z;IJZ>uC8kJnksL-r+R7N#SAbL)8)~)yGX6Ct*TpD?e$jI1RisS9#R6lZX(7-L9`C9 zh6n5}($PWqUePpK3uC9$P*b;J*?Tn&Q16EKDRH)+EG}VvZl;YwJ?eYv3iV!fxayy( zx$V-RnPzZ1*E5UEy5*Nkf}X|`y7D)h(?;Y#ny$K;iAs`Oa; zqiQ{Sfc3Bz=C-g?ct^2WF_r(G|AQbaF01lX3guswTa=|rfv=+8{2kjD&)|%oCDLwu ze?d&Zb%y`*qbrwdR^iA3d%&zw`n4=X2o9C*_IY zN&}xZivw2%Mz?c==~Vn=?sV{F>S71@Qmd#67(bb`-H~UZlOUaj`}0IF=aab(a0js@ z?=0q;EwGjo@5vA46XtN>%vU2(yLwIKd)4)gp5>MGRnRORU!4~-VN?wtM^+D%uAgP( zV<%4J1jFk27_!emP5{F?$TbAaTvC8I`>V4#@zw$nTzqVC99mgh@9|dEt*u@XSSAXV zDd23c%A#PS2K-4`BG@>Hj=FHi}}Lm9}1Cdp&+fP7l6msiO1^I3Z)cD z(hO?4HHS0@G%Xshrd;!mCR^jun8h*izIa*OEAAA%;zDtzI7K}O%f0z)57*|Y1-qN0 zRmD!cb4#S);%Qa0&W6uz$;+7xvNn<_Gi5-K62T1A`Fkq5xdW zGH-V`P2D`6N!1V6e3w1899h#q&k4slEf$m-XcESpN=SjAECz3Di3v-F(COIp{P_3? z-T%~jw&!49<};r;31n9SzSd$?BtrOBJbv6_@;L))98=LoQKx*}BQRra)|VdqdB>1d zNPwI+mNDt$A&9kKd|uZ+KC;>WxOKeWY8K)G*=S~wAr^vo7gOv zV+^QClbNH}ySuu&ga`Z9P%pbYKWBp-pgS{|vM*L9=GXOvd_aBj^hJ5=2ZBA&E}c0Q z9t}ZODBiQxOh@69TW#?5y|p!*j>Ipw+R`EdT?J<2uDvsSNBnL6=La7>FFn$!vx4qW zT=c&0?V+v=u+$IDT1z-+Z>8aqZ6-w+goUa2-EG$Cp%6y%%(q2UL~q~F@RN2i!~#-U z6f-dAhfXSl`smS`oX21E&!ZlR$PAeX?VqHhJz4Fo^Ia^yzRl%Q1%ql3GdU(gWe9}9 zo62vjGwC5rF);lLvV5JDN97AQpAJ2tPWpJG4iZUvoc&vqPsdbrH#JyzIh#eFE?})yxidF z=<{zlJ-T~c@+mG=hHgt!Ha$+u@vj;3`Pbe&@4vmq-`PAk>ZPuZ4UTRc?DWmd-ak6* zA7Rf7P0z_55s(CPB9dpG)m-hQlvnypW(^3gB<5&&)?pS_CVw|*>RW%OK@bDmO~kg3 z(|wl9{hwtwelunHvbLrhS&Uf`m}X{H?H*kyQny1+j4!`&^9FTK`0tpIz(cNV64RU1 zFnHSQ^|m(+^e*Wg@jc6aW^L~{(J?&rdr$pP*o&5Xqw+L62gSuED0UBf+qZ>H{iy9= z>xN}xXTBPii-+$upJ^C-zOljV3t^!yErdC-Xv}}e&(yY!eZ4unx72g~uE;;Pfjyoe zI*z~ z!3Lrk^aOexfi8$3NK4>G1Sv8>vP6)iAxOk$TP@KRfeg$Ff=N7qi6aPM35@L{t%{WY E0j60l;Q#;t diff --git a/services/ai/docs/nextstep-v2.md b/services/ai/docs/nextstep-v2.md new file mode 100644 index 0000000..5609051 --- /dev/null +++ b/services/ai/docs/nextstep-v2.md @@ -0,0 +1,340 @@ +# AI 模块 nextstep-v2 + +> 模块:ai | gRPC 50058 | HTTP 3008 | Python (FastAPI + gRPC aio) +> 更新时间:2026-07-14 +> 前序文档:[nextstep.md](./nextstep.md)(v1,8 RPC / 10 端点) + +--- + +## §1 当前状态 + +v2 新增第 9 个 RPC `GenerateReport`(学情报告生成),满足 teacher-bff `generateReport` mutation 需求。9 RPC 全部实现,402 个测试通过,覆盖率 88.5%。 + +### v2 新增 + +- [x] ai.proto 新增 `GenerateReport` RPC + `GenerateReportRequest` / `GeneratedReport` message +- [x] `ReportService` 业务编排层(data-ana 学情数据 → LLM 生成报告 → 结构化提取摘要+建议) +- [x] gRPC servicer `GenerateReport` 方法(方案 B 降级) +- [x] HTTP 端点 `POST /v1/ai/generate/report`(权限 `ai:report:generate`) +- [x] 权限点 `PERMISSION_AI_REPORT_GENERATE`(teacher + admin 角色) +- [x] proto_gen 重新生成(ai_pb2.py / ai_pb2_grpc.py) +- [x] 测试覆盖(servicer 5 用例 + service 9 用例 + HTTP 5 用例 + 模型 5 用例 + 权限 2 用例) + +### 完整 9 RPC 清单 + +| # | RPC | 类型 | 用途 | 状态 | +| --- | ---------------------- | ---- | --------------------------- | --------- | +| 1 | Chat | 一元 | 非流式聊天 | ✅ 已实现 | +| 2 | StreamChat | 流式 | 流式聊天(SSE over gRPC) | ✅ 已实现 | +| 3 | GenerateQuestion | 一元 | 生成题目 | ✅ 已实现 | +| 4 | StreamGenerateQuestion | 流式 | 流式生成题目 | ✅ 已实现 | +| 5 | OptimizeExpression | 一元 | 优化表达 | ✅ 已实现 | +| 6 | GenerateLessonPlan | 一元 | 启动备课工作流 | ✅ 已实现 | +| 7 | GetLessonPlanStatus | 一元 | 查询备课工作流状态 | ✅ 已实现 | +| 8 | ConfirmLessonPlan | 一元 | 确认备课结果入库 | ✅ 已实现 | +| 9 | **GenerateReport** | 一元 | **生成学情报告(v2 新增)** | ✅ 已实现 | + +### 完整 11 HTTP 端点清单 + +| # | 端点 | 方法 | 权限 | 状态 | +| --- | --------------------------------- | ---- | ---------------------- | --------- | +| 1 | `/healthz` | GET | - | ✅ 已实现 | +| 2 | `/readyz` | GET | - | ✅ 已实现 | +| 3 | `/v1/ai/chat` | POST | ai:chat | ✅ 已实现 | +| 4 | `/v1/ai/chat/stream` | POST | ai:chat | ✅ 已实现 | +| 5 | `/v1/ai/generate/question` | POST | ai:question:generate | ✅ 已实现 | +| 6 | `/v1/ai/generate/question/stream` | POST | ai:question:generate | ✅ 已实现 | +| 7 | `/v1/ai/optimize/expression` | POST | ai:expression:optimize | ✅ 已实现 | +| 8 | `/v1/ai/lesson-plan/generate` | POST | ai:lesson:generate | ✅ 已实现 | +| 9 | `/v1/ai/lesson-plan/status/{id}` | GET | - | ✅ 已实现 | +| 10 | `/v1/ai/lesson-plan/confirm/{id}` | POST | ai:lesson:confirm | ✅ 已实现 | +| 11 | **`/v1/ai/generate/report`** | POST | **ai:report:generate** | ✅ 已实现 | + +### 权限点清单 + +| 权限点 | teacher | admin | student | 用途 | +| ---------------------- | ------- | ----- | ------- | ---------------- | +| ai:chat | ✅ | ✅ | ✅ | 聊天 | +| ai:question:generate | ✅ | ✅ | ❌ | 生成题目 | +| ai:expression:optimize | ✅ | ✅ | ❌ | 优化表达 | +| ai:lesson:generate | ✅ | ✅ | ❌ | 生成教案 | +| ai:lesson:confirm | ✅ | ✅ | ❌ | 确认教案 | +| **ai:report:generate** | ✅ | ✅ | ❌ | **生成学情报告** | + +--- + +## §2 GenerateReport 实现详情 + +### §2.1 数据流 + +``` +teacher-bff generateReport mutation + → ai gRPC GenerateReport(:50058) + → ReportService.generate() + → data-ana gRPC GetClassPerformance / GetStudentWeakness / GetLearningTrend(:50055) + → Prompt 组装(PromptTemplateService 或内联 fallback) + → LLM FailoverChain.chat()(OpenAI → Anthropic → 百川 → Ollama) + → 结构化提取(摘要 + 教学建议) + ← GeneratedReport { id, content, summary, recommendations, degraded, degraded_reason } +``` + +### §2.2 报告类型 + +| report_type | 用途 | 必填参数 | data-ana 数据源 | +| -------------- | ---------------- | --------------------- | ----------------------------------------------------------- | +| class_summary | 班级学情总结 | class_id | GetClassPerformance | +| student_detail | 学生个人学情详情 | class_id + student_id | GetClassPerformance + GetStudentWeakness + GetLearningTrend | +| exam_analysis | 考试分析 | class_id | GetClassPerformance | + +### §2.3 降级策略 + +| 场景 | 降级行为 | degraded | degraded_reason | +| -------------------- | ------------------------------------------------ | -------- | -------------------------------- | +| LLM 全部不可用 | 返回空 content + 空 summary + 空 recommendations | true | "LLM unavailable: ..." | +| data-ana 不可用 | 上下文降级(空数据),LLM 仍可生成(基于空数据) | false | (报告内容会说明无数据) | +| ReportService 未注入 | gRPC 返回空 GeneratedReport | true | "report_service not initialized" | +| 未知异常 | gRPC 抛 AIError(AI_INTERNAL_ERROR) | - | - | + +--- + +## §3 上游依赖(ai 依赖谁) + +### §3.1 gRPC 同步调用 + +| 被调用方 | 端口 | Service.RPC | 用途 | 状态 | +| -------- | ----- | -------------------------------------- | ------------------------------------------- | --------- | +| content | 50054 | KnowledgeGraphService.GetPrerequisites | 查询知识点前置依赖(备课工作流 Step 2) | ✅ 已实现 | +| content | 50054 | KnowledgeGraphService.GetLearningPath | 查询学习路径(备课工作流 Step 2) | ✅ 已实现 | +| content | 50054 | QuestionService.BatchCreateQuestions | 批量创建题目入库(备课工作流 Confirm) | ✅ 已实现 | +| data-ana | 50055 | AnalyticsService.GetClassPerformance | 查询班级学情(备课工作流 + **学情报告**) | ✅ 已实现 | +| data-ana | 50055 | AnalyticsService.GetStudentWeakness | 查询学生薄弱点(备课工作流 + **学情报告**) | ✅ 已实现 | +| data-ana | 50055 | AnalyticsService.GetLearningTrend | 查询学习趋势(备课工作流 + **学情报告**) | ✅ 已实现 | +| iam | 50052 | IamService.GetEffectiveDataScope | 查询用户数据范围(多租户配额) | ✅ 已实现 | + +> **v2 变更**:data-ana 的 3 个 RPC 现在同时服务于备课工作流和学情报告两条业务线。 + +### §3.2 基础设施依赖 + +| 依赖 | 用途 | 状态 | +| ----------------------- | ---------------------------------------------- | --------- | +| Redis | 限流 + 工作流状态存储 + 用量记录 | ✅ 已实现 | +| Kafka | AIUsageEvent 事件发布(topic: `edu.ai.usage`) | ✅ 已实现 | +| OpenTelemetry Collector | 链路追踪 + 指标导出 | ✅ 已实现 | + +### §3.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` | 本地降级 | ✅ 已实现 | + +--- + +## §4 下游就绪信号(谁依赖 ai) + +### §4.1 teacher-bff — 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 | ✅ 已就绪 | +| **AiService.GenerateReport 可调用** | gRPC 50058 | ✅ **已就绪** | + +> teacher-bff 通过 `AI_GRPC_TARGET=ai:50058` 连接,留空时走降级模式 B。 + +### §4.2 student-bff — P1 + +| 就绪标志 | 消费方式 | 状态 | +| --------------------------------------- | ---------- | --------- | +| AiService.Chat 可调用(同步 AI 答疑) | gRPC 50058 | ✅ 已就绪 | +| AiService.StreamChat 可调用(流式答疑) | gRPC 50058 | ✅ 已就绪 | + +### §4.3 api-gateway — P2 + +| 就绪标志 | 消费方式 | 状态 | +| -------------------------- | ---------------------------------------- | --------- | +| ai HTTP 3008 /healthz 可达 | HTTP 反向代理 `/api/v1/ai/*` → `ai:3008` | ✅ 已就绪 | + +### §4.4 data-ana — P2 + +| 就绪标志 | 消费方式 | 状态 | +| ------------------------------------- | -------------------------------------- | --------- | +| Kafka topic `edu.ai.usage` 有事件发布 | CDC 消费者 → ClickHouse `ai_usage_log` | ✅ 已就绪 | + +--- + +## §5 需要上下游实现的工作(nextstep-v2 新增) + +### §5.1 teacher-bff 需要完成 + +| # | 工作项 | 当前状态 | 优先级 | +| --- | -------------------------------------------------------- | ---------------------------- | ------ | +| 1 | AI gRPC 客户端添加 `generateReport` 方法 | 当前返回降级响应(degraded) | P1 | +| 2 | `teacher.service.ts` 的 `generateReport()` 调用真实 gRPC | 当前硬编码降级响应 | P1 | +| 3 | GraphQL schema `GeneratedReport` type 字段对齐 proto | 需确认字段一致性 | P2 | + +**详细说明**: + +teacher-bff 的 `ai-grpc.client.ts` 目前实现了 `chat` / `streamChat` / `generateQuestion` / `optimizeExpression`,但 `generateLessonPlan` 和 `generateReport` 走降级。现在 ai 侧 `GenerateReport` RPC 已就绪,teacher-bff 需: + +```typescript +// ai-grpc.client.ts 需添加 +async generateReport(input: GenerateReportInput): Promise { + const request = new GenerateReportRequest({ + classId: input.classId, + reportType: input.reportType, + studentId: input.studentId ?? undefined, + userId: input.userId, + dataScope: input.dataScope ?? undefined, + }); + const response = await this.client.generateReport(request); + return { + id: response.id, + content: response.content, + summary: response.summary, + recommendations: response.recommendations, + degraded: response.degraded, + degradedReason: response.degradedReason, + }; +} +``` + +ai.proto `GenerateReportRequest` 字段: + +```protobuf +message GenerateReportRequest { + string class_id = 1; + string report_type = 2; // class_summary / student_detail / exam_analysis + optional string student_id = 3; // student_detail 时必填 + string user_id = 4; + string data_scope = 5; // JSON 序列化的 DataScope +} + +message GeneratedReport { + string id = 1; + string content = 2; // 报告正文(Markdown) + string summary = 3; // 摘要 + repeated string recommendations = 4; // 教学建议 + bool degraded = 5; + string degraded_reason = 6; +} +``` + +### §5.2 student-bff 需要确认 + +| # | 工作项 | 当前状态 | 优先级 | +| --- | ------------------------------------------- | -------------------------------- | ------ | +| 1 | 确认 `ChatService.Chat` gRPC 调用可用 | ai 侧已就绪,待 student-bff 确认 | P1 | +| 2 | 确认 `ChatService.StreamChat` gRPC 调用可用 | ai 侧已就绪,待 student-bff 确认 | P1 | + +> student-bff nextstep-v2.md §5.4 标注这两个 RPC "需确认"。ai 侧 `Chat` 和 `StreamChat` RPC 已在 v1 实现,gRPC 50058 可直接调用。 + +### §5.3 data-ana 需要保持 + +| # | 工作项 | 当前状态 | 优先级 | +| --- | ------------------------------------------------ | --------- | ------ | +| 1 | AnalyticsService 3 RPC 保持可用 | ✅ 已就绪 | P1 | +| 2 | GenerateReport 依赖 GetClassPerformance 等 3 RPC | ✅ 已就绪 | P1 | + +> 学情报告功能依赖 data-ana 的 3 个 AnalyticsService RPC。如果 data-ana 不可用,报告会基于降级上下文(空数据)生成,但 LLM 仍可工作。 + +### §5.4 api-gateway 需要保持 + +| # | 工作项 | 当前状态 | 优先级 | +| --- | ------------------------------------------------------------ | --------- | ------ | +| 1 | `/api/v1/ai/*` → `ai:3008` 反向代理路由保持可用 | ✅ 已就绪 | P2 | +| 2 | 新增 `/v1/ai/generate/report` 自动被 `/api/v1/ai/*` 通配覆盖 | ✅ 已就绪 | P2 | + +> api-gateway 使用通配路由 `/api/v1/ai/*`,新增的 `/v1/ai/generate/report` 端点自动被覆盖,无需额外配置。 + +--- + +## §6 联调待办 + +| # | 联调项 | 联调方 | 阻塞条件 | 状态 | +| --- | -------------------------------------- | ----------- | ------------------------------------ | ------------------------------------------- | +| 1 | ai gRPC + teacher-bff SSE 联调 | teacher-bff | ai 服务容器启动 | ✅ ai 侧就绪(待 teacher-bff 接入) | +| 2 | ai gRPC StreamChat + student-bff 联调 | student-bff | ai 服务容器启动 | ✅ ai 侧就绪(待 student-bff 确认) | +| 3 | ai /healthz + api-gateway 联调 | api-gateway | ai 服务容器启动 | ✅ ai 侧就绪(待 api-gateway 路由) | +| 4 | AIUsageEvent + data-ana CDC 消费联调 | data-ana | ai Kafka 生产 + data-ana 消费 | ✅ ai 生产就绪(待 data-ana 消费) | +| 5 | ai ↔ content gRPC 联调 | content | 双方容器启动 | ✅ ai 客户端就绪(待 content 启动) | +| 6 | ai ↔ data-ana gRPC 联调 | data-ana | 双方容器启动 | ✅ ai 客户端就绪(待 data-ana 启动) | +| 7 | ai ↔ iam gRPC 联调 | iam | 双方容器启动 | ✅ ai 客户端就绪(待 iam 启动) | +| 8 | **GenerateReport + teacher-bff 联调** | teacher-bff | ai RPC 就绪 + teacher-bff 客户端更新 | ✅ ai RPC 就绪(待 teacher-bff 客户端更新) | +| 9 | **GenerateReport + data-ana 数据联调** | data-ana | 双方容器启动 + 真实学情数据 | ✅ ai 客户端就绪(待 data-ana 有数据) | + +--- + +## §7 测试验证 + +### §7.1 单元测试 + +| 测试文件 | 新增用例数 | 覆盖范围 | +| --------------------- | ---------- | -------------------------------------------------------------------------------------- | +| test_grpc_servicer.py | 5 | GenerateReport 成功/学生详情/未初始化降级/LLM降级/内部错误 | +| test_services.py | 9 | ReportService 生成成功/学生详情/LLM降级/无data-ana/摘要提取/建议提取(2)/prompt构建 | +| test_main_app.py | 5 | HTTP 端点 class_summary/student_detail/invalid_type/missing_class_id/permission_denied | +| test_models.py | 5 | report 模型 默认值/invalid_type/missing_class_id/student_detail/data_defaults | +| test_permission.py | 2 | teacher 有 report 权限 / student 无 report 权限 | +| **合计** | **26** | v1 377 → v2 402(+25 净增,1 个修复 _extract_recommendations bug) | + +### §7.2 质量校验 + +| 校验项 | 结果 | 详情 | +| ---------- | -------- | -------------------------------------------- | +| ruff check | ✅ 通过 | 零警告 | +| pytest | ✅ 通过 | 402 passed, 0 failed | +| 覆盖率 | ✅ 88.5% | report_service.py 91%,models/report.py 100% | + +### §7.3 Docker 验证(待执行) + +> arch.db 本次不扫描,Docker 验证待 infra 环境就绪后执行。 + +| # | 验证项 | 状态 | +| --- | --------------------- | --------- | +| 1 | Docker 镜像构建 | ⏳ 待验证 | +| 2 | 容器启动 + gRPC 50058 | ⏳ 待验证 | +| 3 | HTTP /healthz | ⏳ 待验证 | +| 4 | HTTP /readyz | ⏳ 待验证 | +| 5 | GenerateReport gRPC | ⏳ 待验证 | + +--- + +## §8 关键实现文件 + +| 文件 | 变更类型 | 说明 | +| -------------------------------------- | -------- | -------------------------------------------- | +| `packages/shared-proto/proto/ai.proto` | 修改 | 新增 GenerateReport RPC + 2 message | +| `src/ai/proto_gen/ai_pb2.py` | 重新生成 | protobuf 代码 | +| `src/ai/proto_gen/ai_pb2_grpc.py` | 重新生成 | gRPC stub/servicer 代码 | +| `src/ai/models/report.py` | 新建 | Pydantic 请求/响应模型 | +| `src/ai/models/__init__.py` | 修改 | 导出 report 模型 | +| `src/ai/services/report_service.py` | 新建 | ReportService 业务编排 | +| `src/ai/services/__init__.py` | 修改 | 导出 ReportService | +| `src/ai/grpc_server/servicer.py` | 修改 | GenerateReport 方法 + report_service 注入 | +| `src/ai/grpc_server/server.py` | 修改 | create_grpc_server 添加 report_service 参数 | +| `src/ai/middleware/permission.py` | 修改 | 新增 PERMISSION_AI_REPORT_GENERATE 权限点 | +| `src/ai/main.py` | 修改 | ReportService 实例化 + HTTP /generate/report | +| `tests/test_grpc_servicer.py` | 修改 | 5 个 GenerateReport servicer 测试 | +| `tests/test_services.py` | 修改 | 9 个 ReportService 测试 | +| `tests/test_main_app.py` | 修改 | 5 个 HTTP 端点测试 | +| `tests/test_models.py` | 修改 | 5 个 report 模型测试 | +| `tests/test_permission.py` | 修改 | 2 个 report 权限测试 | + +--- + +## §9 P6+ 待评估 + +| # | 待评估项 | 说明 | +| --- | ----------------- | ------------------------------------------------------------------ | +| 1 | Temporal 引入评估 | 备课工作流 P5 用 BackgroundTasks + Redis,P6 评估是否引入 Temporal | +| 2 | content 事件订阅 | P5 不订阅 content 事件,P6+ 评估是否需要知识点变更事件驱动 | +| 3 | MockLLMProvider | 02 文档提到但未实现,测试环境用 httpx Mock 替代 | +| 4 | 报告持久化 | 当前 GenerateReport 不持久化报告,P6+ 评估是否需要入库 | +| 5 | 报告模板管理 | 当前用内联 fallback 模板,P6+ 评估是否需要独立模板管理服务 | +| 6 | 报告异步生成 | 当前同步生成,长报告可能超时,P6+ 评估是否改为异步 + 轮询模式 | diff --git a/services/ai/src/ai/grpc_server/server.py b/services/ai/src/ai/grpc_server/server.py index 1f96393..daf5e6d 100644 --- a/services/ai/src/ai/grpc_server/server.py +++ b/services/ai/src/ai/grpc_server/server.py @@ -65,6 +65,7 @@ def create_grpc_server( question_service: Any = None, expression_service: Any = None, workflow_service: Any = None, + report_service: Any = None, ) -> GrpcServer: """创建 gRPC server(工厂函数).""" servicer = AiServicer( @@ -72,5 +73,6 @@ def create_grpc_server( question_service=question_service, expression_service=expression_service, workflow_service=workflow_service, + report_service=report_service, ) return GrpcServer(port=port, servicer=servicer) diff --git a/services/ai/src/ai/grpc_server/servicer.py b/services/ai/src/ai/grpc_server/servicer.py index 7a090b2..9fc9d42 100644 --- a/services/ai/src/ai/grpc_server/servicer.py +++ b/services/ai/src/ai/grpc_server/servicer.py @@ -1,4 +1,4 @@ -"""AiService gRPC Servicer(8 RPC 实现). +"""AiService gRPC Servicer(9 RPC 实现). 所有 RPC 返回 protobuf message,降级采用方案 B(degraded 字段在 message 内)。 业务逻辑由注入的 service 层处理,servicer 仅做 proto ↔ domain 模型转换。 @@ -25,6 +25,7 @@ class AiServicer(ai_pb2_grpc.AiServiceServicer): - question_service: QuestionService(GenerateQuestion / StreamGenerateQuestion) - expression_service: ExpressionService(OptimizeExpression) - workflow_service: LessonPlanWorkflowService(备课工作流) + - report_service: ReportService(学情报告) """ def __init__( @@ -33,11 +34,13 @@ class AiServicer(ai_pb2_grpc.AiServiceServicer): question_service: Any = None, expression_service: Any = None, workflow_service: Any = None, + report_service: Any = None, ) -> None: self._chat_service = chat_service self._question_service = question_service self._expression_service = expression_service self._workflow_service = workflow_service + self._report_service = report_service async def Chat( # noqa: N802 - gRPC RPC 方法名必须匹配 proto 定义 self, @@ -46,10 +49,7 @@ class AiServicer(ai_pb2_grpc.AiServiceServicer): ) -> ai_pb2.ChatResponse: """非流式聊天.""" ctx = get_user_context(context) - messages = [ - {"role": m.role, "content": m.content} - for m in request.messages - ] + messages = [{"role": m.role, "content": m.content} for m in request.messages] try: if self._chat_service is None: return _degraded_chat_response(request.model, "chat_service not initialized") @@ -88,10 +88,7 @@ class AiServicer(ai_pb2_grpc.AiServiceServicer): ) -> AsyncGenerator[ai_pb2.ChatChunk, None]: """流式聊天(SSE over gRPC).""" ctx = get_user_context(context) - messages = [ - {"role": m.role, "content": m.content} - for m in request.messages - ] + messages = [{"role": m.role, "content": m.content} for m in request.messages] try: if self._chat_service is None: yield ai_pb2.ChatChunk(content="[degraded] chat_service not initialized", done=True) @@ -322,6 +319,43 @@ class AiServicer(ai_pb2_grpc.AiServiceServicer): logger.error("confirm_lesson_plan_rpc_error", error=str(exc)) raise AIError(ErrorCode.AI_INTERNAL_ERROR, str(exc)) from exc + async def GenerateReport( # noqa: N802 - gRPC RPC 方法名必须匹配 proto 定义 + self, + request: ai_pb2.GenerateReportRequest, + context: grpc.ServicerContext, + ) -> ai_pb2.GeneratedReport: + """生成学情报告.""" + ctx = get_user_context(context) + try: + if self._report_service is None: + return ai_pb2.GeneratedReport( + id="", + content="", + summary="", + degraded=True, + degraded_reason="report_service not initialized", + ) + result = await self._report_service.generate( + class_id=request.class_id, + report_type=request.report_type, + student_id=request.student_id if request.HasField("student_id") else None, + user_id=request.user_id or ctx.user_id, + data_scope=request.data_scope or ctx.data_scope, + ) + return ai_pb2.GeneratedReport( + id=result.id, + content=result.content, + summary=result.summary, + recommendations=result.recommendations, + degraded=result.degraded, + degraded_reason=result.degraded_reason, + ) + except AIError: + raise + except Exception as exc: # noqa: BLE001 + logger.error("generate_report_rpc_error", error=str(exc)) + raise AIError(ErrorCode.AI_INTERNAL_ERROR, str(exc)) from exc + def _degraded_chat_response(model: str, reason: str) -> ai_pb2.ChatResponse: """构建降级聊天响应.""" diff --git a/services/ai/src/ai/main.py b/services/ai/src/ai/main.py index 9ac81a4..96daa56 100644 --- a/services/ai/src/ai/main.py +++ b/services/ai/src/ai/main.py @@ -1,8 +1,8 @@ """AI 网关服务入口. 整合组件(02-architecture-design.md §1.2 完整分层): - - HTTP 端点(/v1/ai 前缀,ActionState 信封,10 端点) - - gRPC server(端口 50058,8 RPC) + - HTTP 端点(/v1/ai 前缀,ActionState 信封,11 端点) + - gRPC server(端口 50058,9 RPC) - LLM Provider FailoverChain(4 适配器 + 熔断 + 故障切换) - Prompt 模板服务(Jinja2 + YAML) - 评估三道防线(RuleValidator + LLMJudge + QualityGate) @@ -10,6 +10,7 @@ - 安全层(PII + 输入清洗 + 输出审核) - 下游 gRPC 客户端(content/data-ana/iam,真实 gRPC 调用) - 备课工作流(4 步编排 + Redis 状态存储) + - 学情报告(class_summary / student_detail / exam_analysis,data-ana 数据源) - 限流(Redis 三维度令牌桶) - OpenTelemetry + Prometheus """ @@ -44,6 +45,7 @@ from .middleware.permission import ( PERMISSION_AI_EXPRESSION_OPTIMIZE, PERMISSION_AI_LESSON_GENERATE, PERMISSION_AI_QUESTION_GENERATE, + PERMISSION_AI_REPORT_GENERATE, ) from .models import ( ChatRequest, @@ -53,6 +55,8 @@ from .models import ( ConfirmResultResponse, GeneratedQuestionResponse, GenerateQuestionRequest, + GenerateReportRequest, + GenerateReportResponse, LessonPreparationData, LessonPreparationRequest, LessonPreparationResponse, @@ -64,7 +68,7 @@ from .models import ( from .prompt_service import PromptTemplateService from .providers import create_failover_chain from .rate_limiter import RateLimiter -from .services import ChatService, ExpressionService, QuestionService +from .services import ChatService, ExpressionService, QuestionService, ReportService from .services.evaluation import QualityGate, RuleValidator from .usage import KafkaProducer, QuotaEnforcer, UsageRecorder from .workflow import LessonPlanWorkflowService, WorkflowStateStore @@ -136,6 +140,14 @@ _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) +# ReportService 依赖 _data_ana_client,必须在客户端实例化后创建 +_report_service = ReportService( + failover_chain=_failover_chain, + prompt_service=_prompt_service, + data_ana_client=_data_ana_client, + default_model=settings.default_chat_model, +) + _state_store = WorkflowStateStore( redis=None, ttl_seconds=settings.workflow_ttl_seconds, @@ -156,6 +168,7 @@ _grpc_server = create_grpc_server( question_service=_question_service, expression_service=_expression_service, workflow_service=_workflow_service, + report_service=_report_service, ) _redis: Redis | None = None @@ -479,4 +492,37 @@ async def confirm_lesson_plan( return ConfirmResultResponse(success=True, data=data, error=None) +@router.post("/generate/report", response_model=GenerateReportResponse) +async def generate_report( + req: GenerateReportRequest, + request: Request, +) -> GenerateReportResponse: + """生成学情报告. + + 支持三种报告类型: + - class_summary: 班级学情总结 + - student_detail: 单个学生学情详情(需 student_id) + - exam_analysis: 考试分析 + + 数据来源:data-ana 服务(班级学情 / 学生薄弱点 / 学习趋势)。 + LLM 不可用时返回 degraded 响应(degraded=true)。 + """ + ctx = extract_user_context(request) + _permission_guard.check(ctx, PERMISSION_AI_REPORT_GENERATE) + await _rate_limiter.check( + user_id=ctx.user_id, + ip=_client_ip(request), + school_id=ctx.school_id, + ) + with tracer.start_as_current_span("generate_report"): + result = await _report_service.generate( + class_id=req.class_id, + report_type=req.report_type, + student_id=req.student_id, + user_id=ctx.user_id, + data_scope=req.data_scope or ctx.data_scope, + ) + return GenerateReportResponse(success=True, data=result, error=None) + + app.include_router(router) diff --git a/services/ai/src/ai/middleware/permission.py b/services/ai/src/ai/middleware/permission.py index f6ae3a0..db7c128 100644 --- a/services/ai/src/ai/middleware/permission.py +++ b/services/ai/src/ai/middleware/permission.py @@ -6,6 +6,7 @@ ai 服务的权限点(对齐 004 Permissions 常量): - ai:expression:optimize: 优化表达 - ai:lesson:generate: 生成教案 - ai:lesson:confirm: 确认教案 + - ai:report:generate: 生成学情报告 全并行模式:dev_mode=true 时跳过权限校验,仅记录警告。 """ @@ -27,6 +28,7 @@ PERMISSION_AI_QUESTION_GENERATE = "ai:question:generate" PERMISSION_AI_EXPRESSION_OPTIMIZE = "ai:expression:optimize" PERMISSION_AI_LESSON_GENERATE = "ai:lesson:generate" PERMISSION_AI_LESSON_CONFIRM = "ai:lesson:confirm" +PERMISSION_AI_REPORT_GENERATE = "ai:report:generate" # 角色 → 权限映射(简化版,P6 迁移到 iam 动态权限) ROLE_PERMISSIONS: dict[str, set[str]] = { @@ -36,6 +38,7 @@ ROLE_PERMISSIONS: dict[str, set[str]] = { PERMISSION_AI_EXPRESSION_OPTIMIZE, PERMISSION_AI_LESSON_GENERATE, PERMISSION_AI_LESSON_CONFIRM, + PERMISSION_AI_REPORT_GENERATE, }, "admin": { PERMISSION_AI_CHAT, @@ -43,6 +46,7 @@ ROLE_PERMISSIONS: dict[str, set[str]] = { PERMISSION_AI_EXPRESSION_OPTIMIZE, PERMISSION_AI_LESSON_GENERATE, PERMISSION_AI_LESSON_CONFIRM, + PERMISSION_AI_REPORT_GENERATE, }, "student": { PERMISSION_AI_CHAT, diff --git a/services/ai/src/ai/models/__init__.py b/services/ai/src/ai/models/__init__.py index 560a5c1..fb16254 100644 --- a/services/ai/src/ai/models/__init__.py +++ b/services/ai/src/ai/models/__init__.py @@ -14,6 +14,12 @@ from .question import ( GenerateQuestionRequest, QuestionType, ) +from .report import ( + GeneratedReportData, + GenerateReportRequest, + GenerateReportResponse, + ReportType, +) from .workflow import ( ConfirmRequest, ConfirmResultData, @@ -39,6 +45,10 @@ __all__ = [ "GeneratedQuestionData", "GeneratedQuestionResponse", "QuestionType", + "GenerateReportRequest", + "GenerateReportResponse", + "GeneratedReportData", + "ReportType", "OptimizeExpressionRequest", "OptimizedExpressionData", "OptimizeExpressionResponse", diff --git a/services/ai/src/ai/models/report.py b/services/ai/src/ai/models/report.py new file mode 100644 index 0000000..c2e146c --- /dev/null +++ b/services/ai/src/ai/models/report.py @@ -0,0 +1,37 @@ +"""学情报告模型.""" + +from typing import Literal + +from pydantic import BaseModel, Field + +from .action_state import ActionState + +ReportType = Literal["class_summary", "student_detail", "exam_analysis"] + + +class GenerateReportRequest(BaseModel): + """学情报告生成请求.""" + + class_id: str = Field(..., description="班级 ID") + report_type: ReportType = Field( + "class_summary", + description="报告类型:class_summary / student_detail / exam_analysis", + ) + student_id: str | None = Field(None, description="学生 ID(student_detail 必填)") + user_id: str | None = None + data_scope: str | None = None + + +class GeneratedReportData(BaseModel): + """学情报告数据(含降级标记).""" + + id: str + content: str + summary: str + recommendations: list[str] = Field(default_factory=list) + degraded: bool = False + degraded_reason: str = "" + + +class GenerateReportResponse(ActionState[GeneratedReportData]): + """学情报告响应信封.""" diff --git a/services/ai/src/ai/proto_gen/ai_pb2.py b/services/ai/src/ai/proto_gen/ai_pb2.py index 57dce70..e519ae6 100644 --- a/services/ai/src/ai/proto_gen/ai_pb2.py +++ b/services/ai/src/ai/proto_gen/ai_pb2.py @@ -24,7 +24,7 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x08\x61i.proto\x12\x14next_edu_cloud.ai.v1\"\xd8\x01\n\x0b\x43hatRequest\x12\x33\n\x08messages\x18\x01 \x03(\x0b\x32!.next_edu_cloud.ai.v1.ChatMessage\x12\r\n\x05model\x18\x02 \x01(\t\x12\x13\n\x0btemperature\x18\x03 \x01(\x01\x12\x14\n\x07user_id\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x17\n\nsession_id\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x17\n\ndata_scope\x18\x06 \x01(\tH\x02\x88\x01\x01\x42\n\n\x08_user_idB\r\n\x0b_session_idB\r\n\x0b_data_scope\",\n\x0b\x43hatMessage\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"\x85\x01\n\x0c\x43hatResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12*\n\x05usage\x18\x03 \x01(\x0b\x32\x1b.next_edu_cloud.ai.v1.Usage\x12\x10\n\x08\x64\x65graded\x18\x04 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x05 \x01(\t\"c\n\x05Usage\x12\x15\n\rprompt_tokens\x18\x01 \x01(\x05\x12\x19\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x12\x14\n\x0ctotal_tokens\x18\x03 \x01(\x05\x12\x12\n\nlatency_ms\x18\x04 \x01(\x05\"*\n\tChatChunk\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x0c\n\x04\x64one\x18\x02 \x01(\x08\"\xd5\x01\n\x17GenerateQuestionRequest\x12\x0e\n\x06prompt\x18\x01 \x01(\t\x12\x0f\n\x07subject\x18\x02 \x01(\t\x12\x12\n\ndifficulty\x18\x03 \x01(\t\x12\x12\n\x05grade\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x1b\n\x13knowledge_point_ids\x18\x05 \x03(\t\x12\x1a\n\rquestion_type\x18\x06 \x01(\tH\x01\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\x05H\x02\x88\x01\x01\x42\x08\n\x06_gradeB\x10\n\x0e_question_typeB\x08\n\x06_count\"\xf1\x01\n\x11GeneratedQuestion\x12\x10\n\x08question\x18\x01 \x01(\t\x12\x0e\n\x06\x61nswer\x18\x02 \x01(\t\x12\x13\n\x0b\x65xplanation\x18\x03 \x01(\t\x12\x15\n\rquestion_type\x18\x04 \x01(\t\x12\x12\n\ndifficulty\x18\x05 \x01(\t\x12\x1b\n\x13knowledge_point_ids\x18\x06 \x03(\t\x12\x1d\n\x10\x65valuation_score\x18\x07 \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x08\x64\x65graded\x18\x08 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\t \x01(\tB\x13\n\x11_evaluation_score\"\x96\x01\n\x16GeneratedQuestionChunk\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x0c\n\x04\x64one\x18\x02 \x01(\x08\x12G\n\x11\x63omplete_question\x18\x03 \x01(\x0b\x32\'.next_edu_cloud.ai.v1.GeneratedQuestionH\x00\x88\x01\x01\x42\x14\n\x12_complete_question\":\n\x19OptimizeExpressionRequest\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontext\x18\x02 \x01(\t\"h\n\x13OptimizedExpression\x12\x11\n\toptimized\x18\x01 \x01(\t\x12\x13\n\x0bsuggestions\x18\x02 \x03(\t\x12\x10\n\x08\x64\x65graded\x18\x03 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x04 \x01(\t\"\xa8\x01\n\x19GenerateLessonPlanRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\x12\r\n\x05topic\x18\x03 \x01(\t\x12\x19\n\x11target_difficulty\x18\x04 \x01(\t\x12\x16\n\x0equestion_count\x18\x05 \x01(\x05\x12\x0f\n\x07user_id\x18\x06 \x01(\t\x12\x12\n\ndata_scope\x18\x07 \x01(\t\"\x8a\x01\n\x12LessonPlanResponse\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12$\n\x1c\x65stimated_completion_seconds\x18\x03 \x01(\x05\x12\x10\n\x08\x64\x65graded\x18\x04 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x05 \x01(\t\"1\n\x1aGetLessonPlanStatusRequest\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\"\xbc\x01\n\x10LessonPlanStatus\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12:\n\tquestions\x18\x03 \x03(\x0b\x32\'.next_edu_cloud.ai.v1.GeneratedQuestion\x12\x12\n\x05\x65rror\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x10\n\x08\x64\x65graded\x18\x05 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x06 \x01(\tB\x08\n\x06_error\"\xbf\x01\n\x18\x43onfirmLessonPlanRequest\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12X\n\rmodifications\x18\x02 \x03(\x0b\x32\x41.next_edu_cloud.ai.v1.ConfirmLessonPlanRequest.ModificationsEntry\x1a\x34\n\x12ModificationsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"^\n\rConfirmResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x1e\n\x16persisted_question_ids\x18\x02 \x03(\t\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error2\xd1\x06\n\tAiService\x12M\n\x04\x43hat\x12!.next_edu_cloud.ai.v1.ChatRequest\x1a\".next_edu_cloud.ai.v1.ChatResponse\x12R\n\nStreamChat\x12!.next_edu_cloud.ai.v1.ChatRequest\x1a\x1f.next_edu_cloud.ai.v1.ChatChunk0\x01\x12j\n\x10GenerateQuestion\x12-.next_edu_cloud.ai.v1.GenerateQuestionRequest\x1a\'.next_edu_cloud.ai.v1.GeneratedQuestion\x12w\n\x16StreamGenerateQuestion\x12-.next_edu_cloud.ai.v1.GenerateQuestionRequest\x1a,.next_edu_cloud.ai.v1.GeneratedQuestionChunk0\x01\x12p\n\x12OptimizeExpression\x12/.next_edu_cloud.ai.v1.OptimizeExpressionRequest\x1a).next_edu_cloud.ai.v1.OptimizedExpression\x12o\n\x12GenerateLessonPlan\x12/.next_edu_cloud.ai.v1.GenerateLessonPlanRequest\x1a(.next_edu_cloud.ai.v1.LessonPlanResponse\x12o\n\x13GetLessonPlanStatus\x12\x30.next_edu_cloud.ai.v1.GetLessonPlanStatusRequest\x1a&.next_edu_cloud.ai.v1.LessonPlanStatus\x12h\n\x11\x43onfirmLessonPlan\x12..next_edu_cloud.ai.v1.ConfirmLessonPlanRequest\x1a#.next_edu_cloud.ai.v1.ConfirmResultb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x08\x61i.proto\x12\x14next_edu_cloud.ai.v1\"\xd8\x01\n\x0b\x43hatRequest\x12\x33\n\x08messages\x18\x01 \x03(\x0b\x32!.next_edu_cloud.ai.v1.ChatMessage\x12\r\n\x05model\x18\x02 \x01(\t\x12\x13\n\x0btemperature\x18\x03 \x01(\x01\x12\x14\n\x07user_id\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x17\n\nsession_id\x18\x05 \x01(\tH\x01\x88\x01\x01\x12\x17\n\ndata_scope\x18\x06 \x01(\tH\x02\x88\x01\x01\x42\n\n\x08_user_idB\r\n\x0b_session_idB\r\n\x0b_data_scope\",\n\x0b\x43hatMessage\x12\x0c\n\x04role\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\"\x85\x01\n\x0c\x43hatResponse\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12*\n\x05usage\x18\x03 \x01(\x0b\x32\x1b.next_edu_cloud.ai.v1.Usage\x12\x10\n\x08\x64\x65graded\x18\x04 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x05 \x01(\t\"c\n\x05Usage\x12\x15\n\rprompt_tokens\x18\x01 \x01(\x05\x12\x19\n\x11\x63ompletion_tokens\x18\x02 \x01(\x05\x12\x14\n\x0ctotal_tokens\x18\x03 \x01(\x05\x12\x12\n\nlatency_ms\x18\x04 \x01(\x05\"*\n\tChatChunk\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x0c\n\x04\x64one\x18\x02 \x01(\x08\"\xd5\x01\n\x17GenerateQuestionRequest\x12\x0e\n\x06prompt\x18\x01 \x01(\t\x12\x0f\n\x07subject\x18\x02 \x01(\t\x12\x12\n\ndifficulty\x18\x03 \x01(\t\x12\x12\n\x05grade\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x1b\n\x13knowledge_point_ids\x18\x05 \x03(\t\x12\x1a\n\rquestion_type\x18\x06 \x01(\tH\x01\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\x05H\x02\x88\x01\x01\x42\x08\n\x06_gradeB\x10\n\x0e_question_typeB\x08\n\x06_count\"\xf1\x01\n\x11GeneratedQuestion\x12\x10\n\x08question\x18\x01 \x01(\t\x12\x0e\n\x06\x61nswer\x18\x02 \x01(\t\x12\x13\n\x0b\x65xplanation\x18\x03 \x01(\t\x12\x15\n\rquestion_type\x18\x04 \x01(\t\x12\x12\n\ndifficulty\x18\x05 \x01(\t\x12\x1b\n\x13knowledge_point_ids\x18\x06 \x03(\t\x12\x1d\n\x10\x65valuation_score\x18\x07 \x01(\x01H\x00\x88\x01\x01\x12\x10\n\x08\x64\x65graded\x18\x08 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\t \x01(\tB\x13\n\x11_evaluation_score\"\x96\x01\n\x16GeneratedQuestionChunk\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x12\x0c\n\x04\x64one\x18\x02 \x01(\x08\x12G\n\x11\x63omplete_question\x18\x03 \x01(\x0b\x32\'.next_edu_cloud.ai.v1.GeneratedQuestionH\x00\x88\x01\x01\x42\x14\n\x12_complete_question\":\n\x19OptimizeExpressionRequest\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontext\x18\x02 \x01(\t\"h\n\x13OptimizedExpression\x12\x11\n\toptimized\x18\x01 \x01(\t\x12\x13\n\x0bsuggestions\x18\x02 \x03(\t\x12\x10\n\x08\x64\x65graded\x18\x03 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x04 \x01(\t\"\xa8\x01\n\x19GenerateLessonPlanRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\x12\r\n\x05topic\x18\x03 \x01(\t\x12\x19\n\x11target_difficulty\x18\x04 \x01(\t\x12\x16\n\x0equestion_count\x18\x05 \x01(\x05\x12\x0f\n\x07user_id\x18\x06 \x01(\t\x12\x12\n\ndata_scope\x18\x07 \x01(\t\"\x8a\x01\n\x12LessonPlanResponse\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12$\n\x1c\x65stimated_completion_seconds\x18\x03 \x01(\x05\x12\x10\n\x08\x64\x65graded\x18\x04 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x05 \x01(\t\"1\n\x1aGetLessonPlanStatusRequest\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\"\xbc\x01\n\x10LessonPlanStatus\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\x12:\n\tquestions\x18\x03 \x03(\x0b\x32\'.next_edu_cloud.ai.v1.GeneratedQuestion\x12\x12\n\x05\x65rror\x18\x04 \x01(\tH\x00\x88\x01\x01\x12\x10\n\x08\x64\x65graded\x18\x05 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x06 \x01(\tB\x08\n\x06_error\"\xbf\x01\n\x18\x43onfirmLessonPlanRequest\x12\x13\n\x0bworkflow_id\x18\x01 \x01(\t\x12X\n\rmodifications\x18\x02 \x03(\x0b\x32\x41.next_edu_cloud.ai.v1.ConfirmLessonPlanRequest.ModificationsEntry\x1a\x34\n\x12ModificationsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"^\n\rConfirmResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x1e\n\x16persisted_question_ids\x18\x02 \x03(\t\x12\x12\n\x05\x65rror\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x08\n\x06_error\"\x8b\x01\n\x15GenerateReportRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x13\n\x0breport_type\x18\x02 \x01(\t\x12\x17\n\nstudent_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x0f\n\x07user_id\x18\x04 \x01(\t\x12\x12\n\ndata_scope\x18\x05 \x01(\tB\r\n\x0b_student_id\"\x83\x01\n\x0fGeneratedReport\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x02 \x01(\t\x12\x0f\n\x07summary\x18\x03 \x01(\t\x12\x17\n\x0frecommendations\x18\x04 \x03(\t\x12\x10\n\x08\x64\x65graded\x18\x05 \x01(\x08\x12\x17\n\x0f\x64\x65graded_reason\x18\x06 \x01(\t2\xb7\x07\n\tAiService\x12M\n\x04\x43hat\x12!.next_edu_cloud.ai.v1.ChatRequest\x1a\".next_edu_cloud.ai.v1.ChatResponse\x12R\n\nStreamChat\x12!.next_edu_cloud.ai.v1.ChatRequest\x1a\x1f.next_edu_cloud.ai.v1.ChatChunk0\x01\x12j\n\x10GenerateQuestion\x12-.next_edu_cloud.ai.v1.GenerateQuestionRequest\x1a\'.next_edu_cloud.ai.v1.GeneratedQuestion\x12w\n\x16StreamGenerateQuestion\x12-.next_edu_cloud.ai.v1.GenerateQuestionRequest\x1a,.next_edu_cloud.ai.v1.GeneratedQuestionChunk0\x01\x12p\n\x12OptimizeExpression\x12/.next_edu_cloud.ai.v1.OptimizeExpressionRequest\x1a).next_edu_cloud.ai.v1.OptimizedExpression\x12o\n\x12GenerateLessonPlan\x12/.next_edu_cloud.ai.v1.GenerateLessonPlanRequest\x1a(.next_edu_cloud.ai.v1.LessonPlanResponse\x12o\n\x13GetLessonPlanStatus\x12\x30.next_edu_cloud.ai.v1.GetLessonPlanStatusRequest\x1a&.next_edu_cloud.ai.v1.LessonPlanStatus\x12h\n\x11\x43onfirmLessonPlan\x12..next_edu_cloud.ai.v1.ConfirmLessonPlanRequest\x1a#.next_edu_cloud.ai.v1.ConfirmResult\x12\x64\n\x0eGenerateReport\x12+.next_edu_cloud.ai.v1.GenerateReportRequest\x1a%.next_edu_cloud.ai.v1.GeneratedReportb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -67,6 +67,10 @@ if not _descriptor._USE_C_DESCRIPTORS: _globals['_CONFIRMLESSONPLANREQUEST_MODIFICATIONSENTRY']._serialized_end=2105 _globals['_CONFIRMRESULT']._serialized_start=2107 _globals['_CONFIRMRESULT']._serialized_end=2201 - _globals['_AISERVICE']._serialized_start=2204 - _globals['_AISERVICE']._serialized_end=3053 + _globals['_GENERATEREPORTREQUEST']._serialized_start=2204 + _globals['_GENERATEREPORTREQUEST']._serialized_end=2343 + _globals['_GENERATEDREPORT']._serialized_start=2346 + _globals['_GENERATEDREPORT']._serialized_end=2477 + _globals['_AISERVICE']._serialized_start=2480 + _globals['_AISERVICE']._serialized_end=3431 # @@protoc_insertion_point(module_scope) diff --git a/services/ai/src/ai/proto_gen/ai_pb2_grpc.py b/services/ai/src/ai/proto_gen/ai_pb2_grpc.py index a1278d3..b0d4bac 100644 --- a/services/ai/src/ai/proto_gen/ai_pb2_grpc.py +++ b/services/ai/src/ai/proto_gen/ai_pb2_grpc.py @@ -3,7 +3,7 @@ import grpc import warnings -from . import ai_pb2 as ai__pb2 +import ai_pb2 as ai__pb2 GRPC_GENERATED_VERSION = '1.82.1' GRPC_VERSION = grpc.__version__ @@ -78,6 +78,11 @@ class AiServiceStub: request_serializer=ai__pb2.ConfirmLessonPlanRequest.SerializeToString, response_deserializer=ai__pb2.ConfirmResult.FromString, _registered_method=True) + self.GenerateReport = channel.unary_unary( + '/next_edu_cloud.ai.v1.AiService/GenerateReport', + request_serializer=ai__pb2.GenerateReportRequest.SerializeToString, + response_deserializer=ai__pb2.GeneratedReport.FromString, + _registered_method=True) class AiServiceServicer: @@ -143,6 +148,13 @@ class AiServiceServicer: context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GenerateReport(self, request, context): + """生成学情报告(班级总结 / 学生详情 / 考试分析) + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_AiServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -186,6 +198,11 @@ def add_AiServiceServicer_to_server(servicer, server): request_deserializer=ai__pb2.ConfirmLessonPlanRequest.FromString, response_serializer=ai__pb2.ConfirmResult.SerializeToString, ), + 'GenerateReport': grpc.unary_unary_rpc_method_handler( + servicer.GenerateReport, + request_deserializer=ai__pb2.GenerateReportRequest.FromString, + response_serializer=ai__pb2.GeneratedReport.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'next_edu_cloud.ai.v1.AiService', rpc_method_handlers) @@ -416,3 +433,30 @@ class AiService: timeout, metadata, _registered_method=True) + + @staticmethod + def GenerateReport(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.ai.v1.AiService/GenerateReport', + ai__pb2.GenerateReportRequest.SerializeToString, + ai__pb2.GeneratedReport.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 index fb17b94..c78fb8f 100644 --- a/services/ai/src/ai/proto_gen/content_pb2.py +++ b/services/ai/src/ai/proto_gen/content_pb2.py @@ -25,7 +25,7 @@ _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') +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(\t\"?\n\x17GetKnowledgePathRequest\x12\x10\n\x08\x63lass_id\x18\x01 \x01(\t\x12\x12\n\nsubject_id\x18\x02 \x01(\t\"\xca\x01\n\x0e\x45lectiveCourse\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\x12\n\nteacher_id\x18\x04 \x01(\t\x12\x10\n\x08\x63\x61pacity\x18\x05 \x01(\x05\x12\x16\n\x0e\x65nrolled_count\x18\x06 \x01(\x05\x12\x0e\n\x06status\x18\x07 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x08 \x01(\t\x12\x12\n\ncreated_at\x18\t \x01(\x03\x12\x12\n\nupdated_at\x18\n \x01(\x03\"\x7f\n\x11\x45lectiveSelection\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nstudent_id\x18\x02 \x01(\t\x12\x11\n\tcourse_id\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x13\n\x0bselected_at\x18\x05 \x01(\x03\x12\x12\n\ndropped_at\x18\x06 \x01(\x03\"`\n#ListAvailableElectiveCoursesRequest\x12\x12\n\nsubject_id\x18\x01 \x01(\t\x12\x11\n\tpage_size\x18\x02 \x01(\x05\x12\x12\n\npage_token\x18\x03 \x01(\t\"r\n\x1bListElectiveCoursesResponse\x12:\n\x07\x63ourses\x18\x01 \x03(\x0b\x32).next_edu_cloud.content.v1.ElectiveCourse\x12\x17\n\x0fnext_page_token\x18\x02 \x01(\t\"L\n&ListElectiveSelectionsByStudentRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x0e\n\x06status\x18\x02 \x01(\t\"b\n\x1eListElectiveSelectionsResponse\x12@\n\nselections\x18\x01 \x03(\x0b\x32,.next_edu_cloud.content.v1.ElectiveSelection\"<\n\x13SelectCourseRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x11\n\tcourse_id\x18\x02 \x01(\t\":\n\x11\x44ropCourseRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x11\n\tcourse_id\x18\x02 \x01(\t\"\xd5\x01\n\nLessonPlan\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nteacher_id\x18\x02 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x03 \x01(\t\x12\x12\n\nsubject_id\x18\x04 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x06 \x01(\t\x12\x0e\n\x06status\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\"[\n\x1fListLessonPlansByTeacherRequest\x12\x12\n\nteacher_id\x18\x01 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x02 \x01(\t\x12\x12\n\nsubject_id\x18\x03 \x01(\t\"G\n\x1fListLessonPlansByStudentRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x02 \x01(\t\"V\n\x17ListLessonPlansResponse\x12;\n\x0clesson_plans\x18\x01 \x03(\x0b\x32%.next_edu_cloud.content.v1.LessonPlan\"\"\n\x14GetLessonPlanRequest\x12\n\n\x02id\x18\x01 \x01(\t\"\xe8\x01\n\nCoursePlan\x12\n\n\x02id\x18\x01 \x01(\t\x12\x12\n\nstudent_id\x18\x02 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x03 \x01(\t\x12\x12\n\nsubject_id\x18\x04 \x01(\t\x12\r\n\x05title\x18\x05 \x01(\t\x12\x11\n\tplan_type\x18\x06 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x07 \x01(\t\x12\x0e\n\x06status\x18\x08 \x01(\t\x12)\n\x08metadata\x18\t \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x12\n\ncreated_at\x18\n \x01(\x03\x12\x12\n\nupdated_at\x18\x0b \x01(\x03\"Z\n\x1fListCoursePlansByStudentRequest\x12\x12\n\nstudent_id\x18\x01 \x01(\t\x12\x10\n\x08\x63lass_id\x18\x02 \x01(\t\x12\x11\n\tplan_type\x18\x03 \x01(\t\"V\n\x17ListCoursePlansResponse\x12;\n\x0c\x63ourse_plans\x18\x01 \x03(\x0b\x32%.next_edu_cloud.content.v1.CoursePlan\"\"\n\x14GetCoursePlanRequest\x12\n\n\x02id\x18\x01 \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\xc9\x04\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\x12o\n\x10GetKnowledgePath\x12\x32.next_edu_cloud.content.v1.GetKnowledgePathRequest\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.SearchQuestionsResponse2\x98\x04\n\x0f\x45lectiveService\x12\x96\x01\n\x1cListAvailableElectiveCourses\x12>.next_edu_cloud.content.v1.ListAvailableElectiveCoursesRequest\x1a\x36.next_edu_cloud.content.v1.ListElectiveCoursesResponse\x12\x9f\x01\n\x1fListElectiveSelectionsByStudent\x12\x41.next_edu_cloud.content.v1.ListElectiveSelectionsByStudentRequest\x1a\x39.next_edu_cloud.content.v1.ListElectiveSelectionsResponse\x12l\n\x0cSelectCourse\x12..next_edu_cloud.content.v1.SelectCourseRequest\x1a,.next_edu_cloud.content.v1.ElectiveSelection\x12\\\n\nDropCourse\x12,.next_edu_cloud.content.v1.DropCourseRequest\x1a .next_edu_cloud.content.v1.Empty2\x96\x03\n\x11LessonPlanService\x12\x8a\x01\n\x18ListLessonPlansByTeacher\x12:.next_edu_cloud.content.v1.ListLessonPlansByTeacherRequest\x1a\x32.next_edu_cloud.content.v1.ListLessonPlansResponse\x12\x8a\x01\n\x18ListLessonPlansByStudent\x12:.next_edu_cloud.content.v1.ListLessonPlansByStudentRequest\x1a\x32.next_edu_cloud.content.v1.ListLessonPlansResponse\x12g\n\rGetLessonPlan\x12/.next_edu_cloud.content.v1.GetLessonPlanRequest\x1a%.next_edu_cloud.content.v1.LessonPlan2\x89\x02\n\x11\x43oursePlanService\x12\x8a\x01\n\x18ListCoursePlansByStudent\x12:.next_edu_cloud.content.v1.ListCoursePlansByStudentRequest\x1a\x32.next_edu_cloud.content.v1.ListCoursePlansResponse\x12g\n\rGetCoursePlan\x12/.next_edu_cloud.content.v1.GetCoursePlanRequest\x1a%.next_edu_cloud.content.v1.CoursePlanb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -102,12 +102,54 @@ if not _descriptor._USE_C_DESCRIPTORS: _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 + _globals['_GETKNOWLEDGEPATHREQUEST']._serialized_start=3807 + _globals['_GETKNOWLEDGEPATHREQUEST']._serialized_end=3870 + _globals['_ELECTIVECOURSE']._serialized_start=3873 + _globals['_ELECTIVECOURSE']._serialized_end=4075 + _globals['_ELECTIVESELECTION']._serialized_start=4077 + _globals['_ELECTIVESELECTION']._serialized_end=4204 + _globals['_LISTAVAILABLEELECTIVECOURSESREQUEST']._serialized_start=4206 + _globals['_LISTAVAILABLEELECTIVECOURSESREQUEST']._serialized_end=4302 + _globals['_LISTELECTIVECOURSESRESPONSE']._serialized_start=4304 + _globals['_LISTELECTIVECOURSESRESPONSE']._serialized_end=4418 + _globals['_LISTELECTIVESELECTIONSBYSTUDENTREQUEST']._serialized_start=4420 + _globals['_LISTELECTIVESELECTIONSBYSTUDENTREQUEST']._serialized_end=4496 + _globals['_LISTELECTIVESELECTIONSRESPONSE']._serialized_start=4498 + _globals['_LISTELECTIVESELECTIONSRESPONSE']._serialized_end=4596 + _globals['_SELECTCOURSEREQUEST']._serialized_start=4598 + _globals['_SELECTCOURSEREQUEST']._serialized_end=4658 + _globals['_DROPCOURSEREQUEST']._serialized_start=4660 + _globals['_DROPCOURSEREQUEST']._serialized_end=4718 + _globals['_LESSONPLAN']._serialized_start=4721 + _globals['_LESSONPLAN']._serialized_end=4934 + _globals['_LISTLESSONPLANSBYTEACHERREQUEST']._serialized_start=4936 + _globals['_LISTLESSONPLANSBYTEACHERREQUEST']._serialized_end=5027 + _globals['_LISTLESSONPLANSBYSTUDENTREQUEST']._serialized_start=5029 + _globals['_LISTLESSONPLANSBYSTUDENTREQUEST']._serialized_end=5100 + _globals['_LISTLESSONPLANSRESPONSE']._serialized_start=5102 + _globals['_LISTLESSONPLANSRESPONSE']._serialized_end=5188 + _globals['_GETLESSONPLANREQUEST']._serialized_start=5190 + _globals['_GETLESSONPLANREQUEST']._serialized_end=5224 + _globals['_COURSEPLAN']._serialized_start=5227 + _globals['_COURSEPLAN']._serialized_end=5459 + _globals['_LISTCOURSEPLANSBYSTUDENTREQUEST']._serialized_start=5461 + _globals['_LISTCOURSEPLANSBYSTUDENTREQUEST']._serialized_end=5551 + _globals['_LISTCOURSEPLANSRESPONSE']._serialized_start=5553 + _globals['_LISTCOURSEPLANSRESPONSE']._serialized_end=5639 + _globals['_GETCOURSEPLANREQUEST']._serialized_start=5641 + _globals['_GETCOURSEPLANREQUEST']._serialized_end=5675 + _globals['_TEXTBOOKSERVICE']._serialized_start=5678 + _globals['_TEXTBOOKSERVICE']._serialized_end=6222 + _globals['_CHAPTERSERVICE']._serialized_start=6225 + _globals['_CHAPTERSERVICE']._serialized_end=6754 + _globals['_KNOWLEDGEGRAPHSERVICE']._serialized_start=6757 + _globals['_KNOWLEDGEGRAPHSERVICE']._serialized_end=7342 + _globals['_QUESTIONSERVICE']._serialized_start=7345 + _globals['_QUESTIONSERVICE']._serialized_end=8253 + _globals['_ELECTIVESERVICE']._serialized_start=8256 + _globals['_ELECTIVESERVICE']._serialized_end=8792 + _globals['_LESSONPLANSERVICE']._serialized_start=8795 + _globals['_LESSONPLANSERVICE']._serialized_end=9201 + _globals['_COURSEPLANSERVICE']._serialized_start=9204 + _globals['_COURSEPLANSERVICE']._serialized_end=9469 # @@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 index ae944d1..2be3424 100644 --- a/services/ai/src/ai/proto_gen/content_pb2_grpc.py +++ b/services/ai/src/ai/proto_gen/content_pb2_grpc.py @@ -532,6 +532,11 @@ class KnowledgeGraphServiceStub: request_serializer=content__pb2.GetLearningPathRequest.SerializeToString, response_deserializer=content__pb2.LearningPath.FromString, _registered_method=True) + self.GetKnowledgePath = channel.unary_unary( + '/next_edu_cloud.content.v1.KnowledgeGraphService/GetKnowledgePath', + request_serializer=content__pb2.GetKnowledgePathRequest.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, @@ -559,6 +564,12 @@ class KnowledgeGraphServiceServicer: context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def GetKnowledgePath(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) @@ -584,6 +595,11 @@ def add_KnowledgeGraphServiceServicer_to_server(servicer, server): request_deserializer=content__pb2.GetLearningPathRequest.FromString, response_serializer=content__pb2.LearningPath.SerializeToString, ), + 'GetKnowledgePath': grpc.unary_unary_rpc_method_handler( + servicer.GetKnowledgePath, + request_deserializer=content__pb2.GetKnowledgePathRequest.FromString, + response_serializer=content__pb2.LearningPath.SerializeToString, + ), 'AddPrerequisite': grpc.unary_unary_rpc_method_handler( servicer.AddPrerequisite, request_deserializer=content__pb2.AddPrerequisiteRequest.FromString, @@ -659,6 +675,33 @@ class KnowledgeGraphService: metadata, _registered_method=True) + @staticmethod + def GetKnowledgePath(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/GetKnowledgePath', + content__pb2.GetKnowledgePathRequest.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, @@ -1085,3 +1128,477 @@ class QuestionService: timeout, metadata, _registered_method=True) + + +class ElectiveServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.ListAvailableElectiveCourses = channel.unary_unary( + '/next_edu_cloud.content.v1.ElectiveService/ListAvailableElectiveCourses', + request_serializer=content__pb2.ListAvailableElectiveCoursesRequest.SerializeToString, + response_deserializer=content__pb2.ListElectiveCoursesResponse.FromString, + _registered_method=True) + self.ListElectiveSelectionsByStudent = channel.unary_unary( + '/next_edu_cloud.content.v1.ElectiveService/ListElectiveSelectionsByStudent', + request_serializer=content__pb2.ListElectiveSelectionsByStudentRequest.SerializeToString, + response_deserializer=content__pb2.ListElectiveSelectionsResponse.FromString, + _registered_method=True) + self.SelectCourse = channel.unary_unary( + '/next_edu_cloud.content.v1.ElectiveService/SelectCourse', + request_serializer=content__pb2.SelectCourseRequest.SerializeToString, + response_deserializer=content__pb2.ElectiveSelection.FromString, + _registered_method=True) + self.DropCourse = channel.unary_unary( + '/next_edu_cloud.content.v1.ElectiveService/DropCourse', + request_serializer=content__pb2.DropCourseRequest.SerializeToString, + response_deserializer=content__pb2.Empty.FromString, + _registered_method=True) + + +class ElectiveServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def ListAvailableElectiveCourses(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 ListElectiveSelectionsByStudent(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 SelectCourse(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 DropCourse(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_ElectiveServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'ListAvailableElectiveCourses': grpc.unary_unary_rpc_method_handler( + servicer.ListAvailableElectiveCourses, + request_deserializer=content__pb2.ListAvailableElectiveCoursesRequest.FromString, + response_serializer=content__pb2.ListElectiveCoursesResponse.SerializeToString, + ), + 'ListElectiveSelectionsByStudent': grpc.unary_unary_rpc_method_handler( + servicer.ListElectiveSelectionsByStudent, + request_deserializer=content__pb2.ListElectiveSelectionsByStudentRequest.FromString, + response_serializer=content__pb2.ListElectiveSelectionsResponse.SerializeToString, + ), + 'SelectCourse': grpc.unary_unary_rpc_method_handler( + servicer.SelectCourse, + request_deserializer=content__pb2.SelectCourseRequest.FromString, + response_serializer=content__pb2.ElectiveSelection.SerializeToString, + ), + 'DropCourse': grpc.unary_unary_rpc_method_handler( + servicer.DropCourse, + request_deserializer=content__pb2.DropCourseRequest.FromString, + response_serializer=content__pb2.Empty.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.ElectiveService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.ElectiveService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class ElectiveService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def ListAvailableElectiveCourses(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.ElectiveService/ListAvailableElectiveCourses', + content__pb2.ListAvailableElectiveCoursesRequest.SerializeToString, + content__pb2.ListElectiveCoursesResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ListElectiveSelectionsByStudent(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.ElectiveService/ListElectiveSelectionsByStudent', + content__pb2.ListElectiveSelectionsByStudentRequest.SerializeToString, + content__pb2.ListElectiveSelectionsResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def SelectCourse(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.ElectiveService/SelectCourse', + content__pb2.SelectCourseRequest.SerializeToString, + content__pb2.ElectiveSelection.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DropCourse(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.ElectiveService/DropCourse', + content__pb2.DropCourseRequest.SerializeToString, + content__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class LessonPlanServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.ListLessonPlansByTeacher = channel.unary_unary( + '/next_edu_cloud.content.v1.LessonPlanService/ListLessonPlansByTeacher', + request_serializer=content__pb2.ListLessonPlansByTeacherRequest.SerializeToString, + response_deserializer=content__pb2.ListLessonPlansResponse.FromString, + _registered_method=True) + self.ListLessonPlansByStudent = channel.unary_unary( + '/next_edu_cloud.content.v1.LessonPlanService/ListLessonPlansByStudent', + request_serializer=content__pb2.ListLessonPlansByStudentRequest.SerializeToString, + response_deserializer=content__pb2.ListLessonPlansResponse.FromString, + _registered_method=True) + self.GetLessonPlan = channel.unary_unary( + '/next_edu_cloud.content.v1.LessonPlanService/GetLessonPlan', + request_serializer=content__pb2.GetLessonPlanRequest.SerializeToString, + response_deserializer=content__pb2.LessonPlan.FromString, + _registered_method=True) + + +class LessonPlanServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def ListLessonPlansByTeacher(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 ListLessonPlansByStudent(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 GetLessonPlan(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_LessonPlanServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'ListLessonPlansByTeacher': grpc.unary_unary_rpc_method_handler( + servicer.ListLessonPlansByTeacher, + request_deserializer=content__pb2.ListLessonPlansByTeacherRequest.FromString, + response_serializer=content__pb2.ListLessonPlansResponse.SerializeToString, + ), + 'ListLessonPlansByStudent': grpc.unary_unary_rpc_method_handler( + servicer.ListLessonPlansByStudent, + request_deserializer=content__pb2.ListLessonPlansByStudentRequest.FromString, + response_serializer=content__pb2.ListLessonPlansResponse.SerializeToString, + ), + 'GetLessonPlan': grpc.unary_unary_rpc_method_handler( + servicer.GetLessonPlan, + request_deserializer=content__pb2.GetLessonPlanRequest.FromString, + response_serializer=content__pb2.LessonPlan.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.LessonPlanService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.LessonPlanService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class LessonPlanService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def ListLessonPlansByTeacher(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.LessonPlanService/ListLessonPlansByTeacher', + content__pb2.ListLessonPlansByTeacherRequest.SerializeToString, + content__pb2.ListLessonPlansResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def ListLessonPlansByStudent(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.LessonPlanService/ListLessonPlansByStudent', + content__pb2.ListLessonPlansByStudentRequest.SerializeToString, + content__pb2.ListLessonPlansResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetLessonPlan(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.LessonPlanService/GetLessonPlan', + content__pb2.GetLessonPlanRequest.SerializeToString, + content__pb2.LessonPlan.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + +class CoursePlanServiceStub: + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.ListCoursePlansByStudent = channel.unary_unary( + '/next_edu_cloud.content.v1.CoursePlanService/ListCoursePlansByStudent', + request_serializer=content__pb2.ListCoursePlansByStudentRequest.SerializeToString, + response_deserializer=content__pb2.ListCoursePlansResponse.FromString, + _registered_method=True) + self.GetCoursePlan = channel.unary_unary( + '/next_edu_cloud.content.v1.CoursePlanService/GetCoursePlan', + request_serializer=content__pb2.GetCoursePlanRequest.SerializeToString, + response_deserializer=content__pb2.CoursePlan.FromString, + _registered_method=True) + + +class CoursePlanServiceServicer: + """Missing associated documentation comment in .proto file.""" + + def ListCoursePlansByStudent(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 GetCoursePlan(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_CoursePlanServiceServicer_to_server(servicer, server): + rpc_method_handlers = { + 'ListCoursePlansByStudent': grpc.unary_unary_rpc_method_handler( + servicer.ListCoursePlansByStudent, + request_deserializer=content__pb2.ListCoursePlansByStudentRequest.FromString, + response_serializer=content__pb2.ListCoursePlansResponse.SerializeToString, + ), + 'GetCoursePlan': grpc.unary_unary_rpc_method_handler( + servicer.GetCoursePlan, + request_deserializer=content__pb2.GetCoursePlanRequest.FromString, + response_serializer=content__pb2.CoursePlan.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'next_edu_cloud.content.v1.CoursePlanService', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('next_edu_cloud.content.v1.CoursePlanService', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class CoursePlanService: + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def ListCoursePlansByStudent(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.CoursePlanService/ListCoursePlansByStudent', + content__pb2.ListCoursePlansByStudentRequest.SerializeToString, + content__pb2.ListCoursePlansResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def GetCoursePlan(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.CoursePlanService/GetCoursePlan', + content__pb2.GetCoursePlanRequest.SerializeToString, + content__pb2.CoursePlan.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 index dd7aa7e..f819bc1 100644 --- a/services/ai/src/ai/proto_gen/iam_pb2.py +++ b/services/ai/src/ai/proto_gen/iam_pb2.py @@ -24,7 +24,7 @@ _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') +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\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\"x\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\"\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(\t\"g\n\x11\x43reateUserRequest\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\x12\x0f\n\x07role_id\x18\x04 \x01(\t\x12\x12\n\ndata_scope\x18\x05 \x01(\t\"e\n\x11UpdateUserRequest\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\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x12\n\ndata_scope\x18\x05 \x01(\t\"$\n\x11\x44\x65leteUserRequest\x12\x0f\n\x07user_id\x18\x01 \x01(\t\"%\n\x12\x44\x65leteUserResponse\x12\x0f\n\x07success\x18\x01 \x01(\x08\x32\x81\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\x12j\n\rBatchGetUsers\x12+.next_edu_cloud.iam.v1.BatchGetUsersRequest\x1a,.next_edu_cloud.iam.v1.BatchGetUsersResponse\x12w\n\x15GetEffectiveDataScope\x12\x33.next_edu_cloud.iam.v1.GetEffectiveDataScopeRequest\x1a).next_edu_cloud.iam.v1.EffectiveDataScope\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\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.ChildrenResponse\x12W\n\nCreateUser\x12(.next_edu_cloud.iam.v1.CreateUserRequest\x1a\x1f.next_edu_cloud.iam.v1.UserInfo\x12W\n\nUpdateUser\x12(.next_edu_cloud.iam.v1.UpdateUserRequest\x1a\x1f.next_edu_cloud.iam.v1.UserInfo\x12\x61\n\nDeleteUser\x12(.next_edu_cloud.iam.v1.DeleteUserRequest\x1a).next_edu_cloud.iam.v1.DeleteUserResponseb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -47,50 +47,50 @@ if not _descriptor._USE_C_DESCRIPTORS: _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 + _globals['_BATCHGETUSERSREQUEST']._serialized_start=519 + _globals['_BATCHGETUSERSREQUEST']._serialized_end=559 + _globals['_BATCHGETUSERSRESPONSE']._serialized_start=561 + _globals['_BATCHGETUSERSRESPONSE']._serialized_end=632 + _globals['_USERINFO']._serialized_start=634 + _globals['_USERINFO']._serialized_end=757 + _globals['_GETEFFECTIVEPERMISSIONSREQUEST']._serialized_start=759 + _globals['_GETEFFECTIVEPERMISSIONSREQUEST']._serialized_end=808 + _globals['_EFFECTIVEPERMISSIONSRESPONSE']._serialized_start=810 + _globals['_EFFECTIVEPERMISSIONSRESPONSE']._serialized_end=861 + _globals['_GETEFFECTIVEACCESSREQUEST']._serialized_start=863 + _globals['_GETEFFECTIVEACCESSREQUEST']._serialized_end=927 + _globals['_EFFECTIVEACCESSRESPONSE']._serialized_start=929 + _globals['_EFFECTIVEACCESSRESPONSE']._serialized_end=991 + _globals['_GETEFFECTIVEDATASCOPEREQUEST']._serialized_start=993 + _globals['_GETEFFECTIVEDATASCOPEREQUEST']._serialized_end=1040 + _globals['_DATASCOPERESPONSE']._serialized_start=1042 + _globals['_DATASCOPERESPONSE']._serialized_end=1081 + _globals['_GETVIEWPORTSREQUEST']._serialized_start=1083 + _globals['_GETVIEWPORTSREQUEST']._serialized_end=1121 + _globals['_VIEWPORTSRESPONSE']._serialized_start=1123 + _globals['_VIEWPORTSRESPONSE']._serialized_end=1198 + _globals['_VIEWPORTITEM']._serialized_start=1200 + _globals['_VIEWPORTITEM']._serialized_end=1320 + _globals['_GETPUBLICKEYREQUEST']._serialized_start=1322 + _globals['_GETPUBLICKEYREQUEST']._serialized_end=1343 + _globals['_PUBLICKEYRESPONSE']._serialized_start=1345 + _globals['_PUBLICKEYRESPONSE']._serialized_end=1414 + _globals['_GETCHILDRENBYPARENTREQUEST']._serialized_start=1416 + _globals['_GETCHILDRENBYPARENTREQUEST']._serialized_end=1463 + _globals['_CHILDRENRESPONSE']._serialized_start=1465 + _globals['_CHILDRENRESPONSE']._serialized_end=1535 + _globals['_CHILDINFO']._serialized_start=1537 + _globals['_CHILDINFO']._serialized_end=1600 + _globals['_EFFECTIVEDATASCOPE']._serialized_start=1602 + _globals['_EFFECTIVEDATASCOPE']._serialized_end=1692 + _globals['_CREATEUSERREQUEST']._serialized_start=1694 + _globals['_CREATEUSERREQUEST']._serialized_end=1797 + _globals['_UPDATEUSERREQUEST']._serialized_start=1799 + _globals['_UPDATEUSERREQUEST']._serialized_end=1900 + _globals['_DELETEUSERREQUEST']._serialized_start=1902 + _globals['_DELETEUSERREQUEST']._serialized_end=1938 + _globals['_DELETEUSERRESPONSE']._serialized_start=1940 + _globals['_DELETEUSERRESPONSE']._serialized_end=1977 + _globals['_IAMSERVICE']._serialized_start=1980 + _globals['_IAMSERVICE']._serialized_end=3517 # @@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 index ccaa252..a2ef03b 100644 --- a/services/ai/src/ai/proto_gen/iam_pb2_grpc.py +++ b/services/ai/src/ai/proto_gen/iam_pb2_grpc.py @@ -63,26 +63,16 @@ class IamServiceStub: 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.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.GetEffectivePermissions = channel.unary_unary( '/next_edu_cloud.iam.v1.IamService/GetEffectivePermissions', request_serializer=iam__pb2.GetEffectivePermissionsRequest.SerializeToString, @@ -93,11 +83,6 @@ class IamServiceStub: 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, @@ -113,6 +98,21 @@ class IamServiceStub: request_serializer=iam__pb2.GetChildrenByParentRequest.SerializeToString, response_deserializer=iam__pb2.ChildrenResponse.FromString, _registered_method=True) + self.CreateUser = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/CreateUser', + request_serializer=iam__pb2.CreateUserRequest.SerializeToString, + response_deserializer=iam__pb2.UserInfo.FromString, + _registered_method=True) + self.UpdateUser = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/UpdateUser', + request_serializer=iam__pb2.UpdateUserRequest.SerializeToString, + response_deserializer=iam__pb2.UserInfo.FromString, + _registered_method=True) + self.DeleteUser = channel.unary_unary( + '/next_edu_cloud.iam.v1.IamService/DeleteUser', + request_serializer=iam__pb2.DeleteUserRequest.SerializeToString, + response_deserializer=iam__pb2.DeleteUserResponse.FromString, + _registered_method=True) class IamServiceServicer: @@ -154,32 +154,20 @@ class IamServiceServicer: 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 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 GetEffectivePermissions(self, request, context): """权限与视口类 """ @@ -193,14 +181,6 @@ class IamServiceServicer: 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) @@ -220,6 +200,25 @@ class IamServiceServicer: context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def CreateUser(self, request, context): + """管理员用户管理类(admin-portal §2.3 P1 阻塞项补齐) + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def UpdateUser(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 DeleteUser(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 = { @@ -248,26 +247,16 @@ def add_IamServiceServicer_to_server(servicer, server): 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, ), + 'GetEffectiveDataScope': grpc.unary_unary_rpc_method_handler( + servicer.GetEffectiveDataScope, + request_deserializer=iam__pb2.GetEffectiveDataScopeRequest.FromString, + response_serializer=iam__pb2.EffectiveDataScope.SerializeToString, + ), 'GetEffectivePermissions': grpc.unary_unary_rpc_method_handler( servicer.GetEffectivePermissions, request_deserializer=iam__pb2.GetEffectivePermissionsRequest.FromString, @@ -278,11 +267,6 @@ def add_IamServiceServicer_to_server(servicer, server): 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, @@ -298,6 +282,21 @@ def add_IamServiceServicer_to_server(servicer, server): request_deserializer=iam__pb2.GetChildrenByParentRequest.FromString, response_serializer=iam__pb2.ChildrenResponse.SerializeToString, ), + 'CreateUser': grpc.unary_unary_rpc_method_handler( + servicer.CreateUser, + request_deserializer=iam__pb2.CreateUserRequest.FromString, + response_serializer=iam__pb2.UserInfo.SerializeToString, + ), + 'UpdateUser': grpc.unary_unary_rpc_method_handler( + servicer.UpdateUser, + request_deserializer=iam__pb2.UpdateUserRequest.FromString, + response_serializer=iam__pb2.UserInfo.SerializeToString, + ), + 'DeleteUser': grpc.unary_unary_rpc_method_handler( + servicer.DeleteUser, + request_deserializer=iam__pb2.DeleteUserRequest.FromString, + response_serializer=iam__pb2.DeleteUserResponse.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'next_edu_cloud.iam.v1.IamService', rpc_method_handlers) @@ -448,87 +447,6 @@ class IamService: 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, @@ -556,6 +474,33 @@ class IamService: 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 GetEffectivePermissions(request, target, @@ -610,33 +555,6 @@ class IamService: 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, @@ -717,3 +635,84 @@ class IamService: timeout, metadata, _registered_method=True) + + @staticmethod + def CreateUser(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/CreateUser', + iam__pb2.CreateUserRequest.SerializeToString, + iam__pb2.UserInfo.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def UpdateUser(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/UpdateUser', + iam__pb2.UpdateUserRequest.SerializeToString, + iam__pb2.UserInfo.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def DeleteUser(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/DeleteUser', + iam__pb2.DeleteUserRequest.SerializeToString, + iam__pb2.DeleteUserResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/services/ai/src/ai/services/__init__.py b/services/ai/src/ai/services/__init__.py index f7dbbf4..d6861ee 100644 --- a/services/ai/src/ai/services/__init__.py +++ b/services/ai/src/ai/services/__init__.py @@ -4,15 +4,18 @@ - ChatService: 聊天(非流式 + 流式) - QuestionService: 题目生成(非流式 + 流式 + 评估三道防线) - ExpressionService: 表达优化 + - ReportService: 学情报告生成(class_summary / student_detail / exam_analysis) - LessonPlanWorkflowService: 备课工作流(M16-2 实现) """ from .chat_service import ChatService from .expression_service import ExpressionService from .question_service import QuestionService +from .report_service import ReportService __all__ = [ "ChatService", "QuestionService", "ExpressionService", + "ReportService", ] diff --git a/services/ai/src/ai/services/report_service.py b/services/ai/src/ai/services/report_service.py new file mode 100644 index 0000000..908c215 --- /dev/null +++ b/services/ai/src/ai/services/report_service.py @@ -0,0 +1,219 @@ +"""学情报告服务. + +编排 LLM Provider FailoverChain + data-ana 学情数据 + Prompt 模板。 +支持 3 种报告类型:class_summary / student_detail / exam_analysis。 +""" + +import json +import time +from typing import Any + +import structlog + +from ..clients.data_ana_client import DataAnaClient +from ..errors import AILLMUnavailableError +from ..models.report import GeneratedReportData, ReportType +from ..prompt_service import PromptTemplateService +from ..providers import ProviderFailoverChain + +logger = structlog.get_logger() + + +class ReportService: + """学情报告生成服务.""" + + def __init__( + self, + failover_chain: ProviderFailoverChain, + prompt_service: PromptTemplateService | None = None, + data_ana_client: DataAnaClient | None = None, + default_model: str = "gpt-4o-mini", + ) -> None: + self._chain = failover_chain + self._prompts = prompt_service + self._data_ana = data_ana_client + self._default_model = default_model + + async def generate( + self, + class_id: str, + report_type: ReportType, + student_id: str | None = None, + user_id: str = "", + data_scope: str = "", + ) -> GeneratedReportData: + """生成学情报告. + + 流程:1) 从 data-ana 拉学情 → 2) 组装 prompt → 3) LLM 生成报告。 + + Returns: + GeneratedReportData(含降级标记) + """ + report_id = f"report-{int(time.time() * 1000)}" + + # 1. 收集学情上下文(data-ana 不可用时降级为空上下文) + context = await self._collect_context(class_id, report_type, student_id) + + # 2. 组装 prompt + prompt = self._build_prompt(report_type, context) + + # 3. 调用 LLM 生成报告 + try: + response = await self._chain.chat( + messages=[ + {"role": "system", "content": self._system_prompt(report_type)}, + {"role": "user", "content": prompt}, + ], + model=self._default_model, + temperature=0.3, + ) + content = response.content + summary = self._extract_summary(content) + recommendations = self._extract_recommendations(content) + return GeneratedReportData( + id=report_id, + content=content, + summary=summary, + recommendations=recommendations, + degraded=False, + degraded_reason="", + ) + except AILLMUnavailableError as exc: + logger.warning("report_degraded_llm", reason=str(exc), report_type=report_type) + return GeneratedReportData( + id=report_id, + content="", + summary="", + recommendations=[], + degraded=True, + degraded_reason=f"LLM unavailable: {exc}", + ) + + async def _collect_context( + self, + class_id: str, + report_type: ReportType, + student_id: str | None, + ) -> dict[str, Any]: + """从 data-ana 收集学情上下文(不可用时返回降级上下文).""" + context: dict[str, Any] = { + "class_id": class_id, + "report_type": report_type, + "student_id": student_id or "", + } + if self._data_ana is None or not self._data_ana.is_available(): + context["degraded"] = True + context["degraded_reason"] = "data-ana client not configured" + return context + + try: + perf = await self._data_ana.get_class_performance(class_id, "") + context["average_score"] = perf.average_score + context["pass_rate"] = perf.pass_rate + context["student_count"] = len(perf.scores) + + if report_type == "student_detail" and student_id: + weakness = await self._data_ana.get_student_weakness(student_id, "") + trend = await self._data_ana.get_learning_trend(student_id) + context["weak_points"] = [ + {"title": wp.title, "mastery": wp.mastery} for wp in weakness.weak_points + ] + context["trend_points"] = [ + {"date": tp.date, "score": tp.score} for tp in trend.points + ] + except Exception as exc: # noqa: BLE001 + logger.warning("report_context_degraded", error=str(exc)) + context["degraded"] = True + context["degraded_reason"] = f"data-ana call failed: {exc}" + + return context + + def _system_prompt(self, report_type: ReportType) -> str: + """系统 prompt(定义 LLM 角色).""" + if self._prompts is not None: + try: + return self._prompts.render( + "report_system", + {"report_type": report_type}, + ) + except Exception: # noqa: BLE001 + pass + return ( + "你是一名专业的教育分析师,负责根据学情数据生成结构化报告。" + "报告必须使用 Markdown 格式,包含「摘要」「详细分析」「教学建议」三部分。" + ) + + def _build_prompt(self, report_type: ReportType, context: dict[str, Any]) -> str: + """构建用户 prompt.""" + if self._prompts is not None: + try: + return self._prompts.render(f"report_{report_type}", context) + except Exception: # noqa: BLE001 + pass + # fallback:内联模板 + context_json = json.dumps(context, ensure_ascii=False, default=str) + type_desc = { + "class_summary": "班级学情总结", + "student_detail": "学生个人学情详情", + "exam_analysis": "考试成绩分析", + }.get(report_type, "学情报告") + return ( + f"请根据以下学情数据生成{type_desc}。\n\n" + f"学情数据:\n{context_json}\n\n" + "要求:\n" + "1. 摘要:100 字以内概述\n" + "2. 详细分析:基于数据的关键发现\n" + "3. 教学建议:3-5 条可执行建议\n" + ) + + def _extract_summary(self, content: str) -> str: + """从报告内容提取摘要(取第一段或前 200 字).""" + if not content: + return "" + # 尝试匹配「摘要」段落 + lines = content.split("\n") + for i, line in enumerate(lines): + if "摘要" in line and i + 1 < len(lines): + return lines[i + 1].strip()[:200] + # fallback:取前 200 字 + return content[:200].strip() + + def _extract_recommendations(self, content: str) -> list[str]: + """从报告内容提取教学建议(匹配列表项). + + 仅在「教学建议」/「建议」章节标题(以 # 开头或独立行)下提取列表项, + 避免列表项自身包含「建议」关键词时被误判为章节标题。 + """ + if not content: + return [] + recommendations: list[str] = [] + in_section = False + for line in content.split("\n"): + stripped = line.strip() + # 仅匹配章节标题(Markdown 标题 # 开头,或纯文本标题行不含列表标记) + is_header = stripped.startswith("#") + is_plain_title = not stripped.startswith(("- ", "* ", "• ", "1.", "2.", "3.")) and ( + "教学建议" in stripped or stripped == "建议" + ) + if is_header and ("教学建议" in stripped or "建议" in stripped): + in_section = True + continue + if is_plain_title: + in_section = True + continue + if in_section: + # 匹配 Markdown 列表项(- 或 1.) + if stripped.startswith(("- ", "* ", "• ")): + recommendations.append(stripped[2:].strip()) + elif stripped and stripped[0].isdigit() and ". " in stripped: + recommendations.append(stripped.split(". ", 1)[1].strip()) + elif stripped.startswith("#"): + # 进入下一个章节 + break + elif not stripped: + # 空行跳过 + continue + else: + # 非列表非标题的非空行 → 章节结束 + break + return recommendations[:5] diff --git a/services/ai/tests/test_grpc_servicer.py b/services/ai/tests/test_grpc_servicer.py index a5edc02..f86868a 100644 --- a/services/ai/tests/test_grpc_servicer.py +++ b/services/ai/tests/test_grpc_servicer.py @@ -1,4 +1,4 @@ -"""gRPC servicer 测试(AiServicer 8 RPC + interceptors 辅助函数).""" +"""gRPC servicer 测试(AiServicer 9 RPC + interceptors 辅助函数).""" from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock @@ -17,22 +17,25 @@ from src.ai.middleware.auth import UserContext from src.ai.models.chat import ChatData, Usage from src.ai.models.expression import OptimizedExpressionData from src.ai.models.question import GeneratedQuestionData +from src.ai.models.report import GeneratedReportData from src.ai.proto_gen import ai_pb2 class TestAiServicer: - """AiServicer 8 RPC 测试.""" + """AiServicer 9 RPC 测试.""" def setup_method(self) -> None: self.chat_svc = AsyncMock() self.question_svc = AsyncMock() self.expr_svc = AsyncMock() self.workflow_svc = AsyncMock() + self.report_svc = AsyncMock() self.servicer = AiServicer( chat_service=self.chat_svc, question_service=self.question_svc, expression_service=self.expr_svc, workflow_service=self.workflow_svc, + report_service=self.report_svc, ) self.context = MagicMock() self.context.user_context = UserContext(user_id="u-1", role="teacher") @@ -43,7 +46,9 @@ class TestAiServicer: async def test_chat_success(self) -> None: self.chat_svc.chat.return_value = ChatData( - content="hi", model="gpt-4o", usage=Usage(), + content="hi", + model="gpt-4o", + usage=Usage(), ) request = ai_pb2.ChatRequest(model="gpt-4o") request.messages.add(role="user", content="hello") @@ -136,7 +141,9 @@ class TestAiServicer: evaluation_score=0.9, ) request = ai_pb2.GenerateQuestionRequest( - prompt="生成加法题", subject="数学", difficulty="easy", + prompt="生成加法题", + subject="数学", + difficulty="easy", ) result = await self.servicer.GenerateQuestion(request, self.context) assert result.question == "1+1=?" @@ -146,7 +153,9 @@ class TestAiServicer: async def test_generate_question_no_service_degraded(self) -> None: servicer = AiServicer(question_service=None) request = ai_pb2.GenerateQuestionRequest( - prompt="生成加法题", subject="数学", difficulty="easy", + prompt="生成加法题", + subject="数学", + difficulty="easy", ) result = await servicer.GenerateQuestion(request, self.context) assert result.degraded is True @@ -155,7 +164,9 @@ class TestAiServicer: async def test_generate_question_llm_unavailable_degraded(self) -> None: self.question_svc.generate.side_effect = AILLMUnavailableError("llm down") request = ai_pb2.GenerateQuestionRequest( - prompt="生成加法题", subject="数学", difficulty="easy", + prompt="生成加法题", + subject="数学", + difficulty="easy", ) result = await self.servicer.GenerateQuestion(request, self.context) assert result.degraded is True @@ -167,7 +178,9 @@ class TestAiServicer: async def test_stream_generate_question_success(self) -> None: complete = ai_pb2.GeneratedQuestion( - question="q", answer="a", question_type="short_answer", + question="q", + answer="a", + question_type="short_answer", ) async def mock_stream_gen(request: object) -> None: @@ -176,7 +189,9 @@ class TestAiServicer: self.question_svc.stream_generate = mock_stream_gen request = ai_pb2.GenerateQuestionRequest( - prompt="生成加法题", subject="数学", difficulty="easy", + prompt="生成加法题", + subject="数学", + difficulty="easy", ) chunks = [] async for chunk in self.servicer.StreamGenerateQuestion(request, self.context): @@ -190,7 +205,9 @@ class TestAiServicer: async def test_stream_generate_question_no_service_degraded(self) -> None: servicer = AiServicer(question_service=None) request = ai_pb2.GenerateQuestionRequest( - prompt="生成加法题", subject="数学", difficulty="easy", + prompt="生成加法题", + subject="数学", + difficulty="easy", ) chunks = [] async for chunk in servicer.StreamGenerateQuestion(request, self.context): @@ -205,7 +222,8 @@ class TestAiServicer: async def test_optimize_expression_success(self) -> None: self.expr_svc.optimize.return_value = OptimizedExpressionData( - optimized="优化后", suggestions=["建议1"], + optimized="优化后", + suggestions=["建议1"], ) request = ai_pb2.OptimizeExpressionRequest(text="原始", context="") result = await self.servicer.OptimizeExpression(request, self.context) @@ -240,8 +258,11 @@ class TestAiServicer: degraded_reason="", ) request = ai_pb2.GenerateLessonPlanRequest( - class_id="c-1", subject_id="math", topic="函数", - target_difficulty="medium", question_count=3, + class_id="c-1", + subject_id="math", + topic="函数", + target_difficulty="medium", + question_count=3, ) result = await self.servicer.GenerateLessonPlan(request, self.context) assert result.workflow_id == "wf-1" @@ -252,7 +273,9 @@ class TestAiServicer: async def test_generate_lesson_plan_no_service_degraded(self) -> None: servicer = AiServicer(workflow_service=None) request = ai_pb2.GenerateLessonPlanRequest( - class_id="c-1", subject_id="math", topic="函数", + class_id="c-1", + subject_id="math", + topic="函数", ) result = await servicer.GenerateLessonPlan(request, self.context) assert result.degraded is True @@ -265,9 +288,13 @@ class TestAiServicer: async def test_get_lesson_plan_status_success(self) -> None: question = GeneratedQuestionData( - question="q1", answer="a1", explanation="e1", - question_type="short_answer", difficulty="easy", - knowledge_point_ids=["kp_1"], evaluation_score=0.8, + question="q1", + answer="a1", + explanation="e1", + question_type="short_answer", + difficulty="easy", + knowledge_point_ids=["kp_1"], + evaluation_score=0.8, ) self.workflow_svc.get_status.return_value = SimpleNamespace( workflow_id="wf-1", @@ -315,6 +342,98 @@ class TestAiServicer: assert result.success is False assert "workflow_service not initialized" in result.error + # ------------------------------------------------------------------ # + # GenerateReport + # ------------------------------------------------------------------ # + + async def test_generate_report_success(self) -> None: + """生成学情报告成功.""" + self.report_svc.generate.return_value = GeneratedReportData( + id="report-1", + content="# 班级学情报告\n\n摘要\n班级整体表现良好。", + summary="班级整体表现良好。", + recommendations=["加强函数概念教学", "增加练习题量"], + degraded=False, + degraded_reason="", + ) + request = ai_pb2.GenerateReportRequest( + class_id="c-1", + report_type="class_summary", + user_id="u-1", + ) + result = await self.servicer.GenerateReport(request, self.context) + assert result.id == "report-1" + assert "班级学情报告" in result.content + assert result.summary == "班级整体表现良好。" + assert list(result.recommendations) == ["加强函数概念教学", "增加练习题量"] + assert result.degraded is False + + async def test_generate_report_student_detail_with_student_id(self) -> None: + """学生详情报告(带 student_id).""" + self.report_svc.generate.return_value = GeneratedReportData( + id="report-2", + content="学生个人报告", + summary="学生薄弱点分析", + recommendations=["针对性练习"], + degraded=False, + degraded_reason="", + ) + request = ai_pb2.GenerateReportRequest( + class_id="c-1", + report_type="student_detail", + student_id="s-001", + user_id="u-1", + ) + result = await self.servicer.GenerateReport(request, self.context) + # 验证 service 被调用时 student_id 正确传递 + self.report_svc.generate.assert_awaited_once() + call_kwargs = self.report_svc.generate.call_args.kwargs + assert call_kwargs["student_id"] == "s-001" + assert call_kwargs["report_type"] == "student_detail" + assert result.id == "report-2" + + async def test_generate_report_no_service_degraded(self) -> None: + """report_service 未初始化时返回降级响应.""" + servicer = AiServicer(report_service=None) + request = ai_pb2.GenerateReportRequest( + class_id="c-1", + report_type="class_summary", + ) + result = await servicer.GenerateReport(request, self.context) + assert result.degraded is True + assert "report_service not initialized" in result.degraded_reason + assert result.id == "" + assert result.content == "" + + async def test_generate_report_llm_unavailable_degraded(self) -> None: + """LLM 不可用时 ReportService 返回降级数据.""" + self.report_svc.generate.return_value = GeneratedReportData( + id="report-3", + content="", + summary="", + recommendations=[], + degraded=True, + degraded_reason="LLM unavailable: all providers failed", + ) + request = ai_pb2.GenerateReportRequest( + class_id="c-1", + report_type="exam_analysis", + ) + result = await self.servicer.GenerateReport(request, self.context) + assert result.degraded is True + assert "LLM unavailable" in result.degraded_reason + + async def test_generate_report_internal_error_raises(self) -> None: + """未知异常转 AI_INTERNAL_ERROR.""" + self.report_svc.generate.side_effect = RuntimeError("boom") + request = ai_pb2.GenerateReportRequest( + class_id="c-1", + report_type="class_summary", + ) + with pytest.raises(AIError) as exc_info: + await self.servicer.GenerateReport(request, self.context) + assert exc_info.value.code == ErrorCode.AI_INTERNAL_ERROR + # ------------------------------------------------------------------ # # degraded response helpers # ------------------------------------------------------------------ # diff --git a/services/ai/tests/test_main_app.py b/services/ai/tests/test_main_app.py index d157053..577e6b1 100644 --- a/services/ai/tests/test_main_app.py +++ b/services/ai/tests/test_main_app.py @@ -328,6 +328,75 @@ async def test_optimize_expression(client: httpx.AsyncClient) -> None: assert body["data"]["degraded"] is True +# --------------------------------------------------------------------------- +# Generate report endpoint +# --------------------------------------------------------------------------- + + +async def test_generate_report_class_summary(client: httpx.AsyncClient) -> None: + """POST /v1/ai/generate/report with class_summary returns degraded report (no LLM key).""" + resp = await client.post( + "/v1/ai/generate/report", + json={"class_id": "c-1", "report_type": "class_summary"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["success"] is True + # 测试环境无 LLM API key → degraded + assert body["data"]["degraded"] is True + assert "id" in body["data"] + + +async def test_generate_report_student_detail(client: httpx.AsyncClient) -> None: + """POST /v1/ai/generate/report with student_detail + student_id.""" + resp = await client.post( + "/v1/ai/generate/report", + json={ + "class_id": "c-1", + "report_type": "student_detail", + "student_id": "s-001", + }, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["success"] is True + assert body["data"]["degraded"] is True + + +async def test_generate_report_invalid_type(client: httpx.AsyncClient) -> None: + """POST /v1/ai/generate/report with invalid report_type returns 422.""" + resp = await client.post( + "/v1/ai/generate/report", + json={"class_id": "c-1", "report_type": "invalid_type"}, + ) + assert resp.status_code == 422 + + +async def test_generate_report_missing_class_id(client: httpx.AsyncClient) -> None: + """POST /v1/ai/generate/report without class_id returns 422.""" + resp = await client.post( + "/v1/ai/generate/report", + json={"report_type": "class_summary"}, + ) + assert resp.status_code == 422 + + +async def test_generate_report_permission_denied(prod_client: httpx.AsyncClient) -> None: + """Student role attempting to generate report returns 403.""" + resp = await prod_client.post( + "/v1/ai/generate/report", + json={"class_id": "c-1", "report_type": "class_summary"}, + headers={ + "X-User-Id": "student-1", + "X-User-Role": "student", + }, + ) + assert resp.status_code == 403 + body = resp.json() + assert body["success"] is False + assert body["error"]["code"] == "AI_FORBIDDEN" + + # --------------------------------------------------------------------------- # Lesson plan endpoints # --------------------------------------------------------------------------- diff --git a/services/ai/tests/test_models.py b/services/ai/tests/test_models.py index 4485877..c59712c 100644 --- a/services/ai/tests/test_models.py +++ b/services/ai/tests/test_models.py @@ -6,6 +6,10 @@ from pydantic import ValidationError from src.ai.models.chat import ChatData, ChatMessage, ChatRequest, Usage from src.ai.models.expression import OptimizeExpressionRequest from src.ai.models.question import GeneratedQuestionData, GenerateQuestionRequest +from src.ai.models.report import ( + GeneratedReportData, + GenerateReportRequest, +) from src.ai.models.workflow import ( ConfirmRequest, LessonPreparationRequest, @@ -52,8 +56,11 @@ class TestChatModels: def test_chat_data_degraded_fields(self) -> None: data = ChatData( - content="x", model="m", usage=Usage(), - degraded=True, degraded_reason="test", + content="x", + model="m", + usage=Usage(), + degraded=True, + degraded_reason="test", ) assert data.degraded is True @@ -131,3 +138,37 @@ class TestWorkflowModels: assert data.questions == [] assert data.error is None assert data.degraded is False + + +class TestReportModels: + """学情报告模型测试.""" + + def test_generate_report_request_defaults(self) -> None: + req = GenerateReportRequest(class_id="c-1") + assert req.report_type == "class_summary" + assert req.student_id is None + assert req.user_id is None + assert req.data_scope is None + + def test_generate_report_request_invalid_type(self) -> None: + with pytest.raises(ValidationError): + GenerateReportRequest(class_id="c-1", report_type="invalid") + + def test_generate_report_request_missing_class_id(self) -> None: + with pytest.raises(ValidationError): + GenerateReportRequest() + + def test_generate_report_request_student_detail(self) -> None: + req = GenerateReportRequest( + class_id="c-1", + report_type="student_detail", + student_id="s-001", + ) + assert req.report_type == "student_detail" + assert req.student_id == "s-001" + + def test_generated_report_data_defaults(self) -> None: + data = GeneratedReportData(id="r-1", content="内容", summary="摘要") + assert data.recommendations == [] + assert data.degraded is False + assert data.degraded_reason == "" diff --git a/services/ai/tests/test_permission.py b/services/ai/tests/test_permission.py index ce1ab06..a5ae409 100644 --- a/services/ai/tests/test_permission.py +++ b/services/ai/tests/test_permission.py @@ -9,6 +9,7 @@ from src.ai.middleware.permission import ( PERMISSION_AI_LESSON_CONFIRM, PERMISSION_AI_LESSON_GENERATE, PERMISSION_AI_QUESTION_GENERATE, + PERMISSION_AI_REPORT_GENERATE, PermissionGuard, ) @@ -39,6 +40,7 @@ class TestPermissionGuard: guard.check(ctx, PERMISSION_AI_QUESTION_GENERATE) guard.check(ctx, PERMISSION_AI_LESSON_GENERATE) guard.check(ctx, PERMISSION_AI_LESSON_CONFIRM) + guard.check(ctx, PERMISSION_AI_REPORT_GENERATE) def test_student_only_chat(self) -> None: """student 角色仅有 chat 权限.""" @@ -49,6 +51,14 @@ class TestPermissionGuard: guard.check(ctx, PERMISSION_AI_QUESTION_GENERATE) assert exc_info.value.code == ErrorCode.AI_FORBIDDEN + def test_student_denied_report(self) -> None: + """student 角色无权生成学情报告.""" + guard = PermissionGuard(dev_mode=False) + ctx = UserContext(user_id="u-1", role="student") + with pytest.raises(AIError) as exc_info: + guard.check(ctx, PERMISSION_AI_REPORT_GENERATE) + assert exc_info.value.code == ErrorCode.AI_FORBIDDEN + def test_unknown_role_defaults_student(self) -> None: """未知角色降级为 student 权限.""" guard = PermissionGuard(dev_mode=False) diff --git a/services/ai/tests/test_services.py b/services/ai/tests/test_services.py index 959311b..874ca62 100644 --- a/services/ai/tests/test_services.py +++ b/services/ai/tests/test_services.py @@ -1,7 +1,8 @@ -"""服务层测试(ChatService / QuestionService / ExpressionService).""" +"""服务层测试(ChatService / QuestionService / ExpressionService / ReportService).""" import json +from src.ai.clients.data_ana_client import DataAnaClientMock from src.ai.models.question import GenerateQuestionRequest from src.ai.providers import ProviderFailoverChain from src.ai.providers.circuit_breaker import CircuitBreaker @@ -9,6 +10,7 @@ from src.ai.services.chat_service import ChatService from src.ai.services.evaluation import QualityGate, RuleValidator from src.ai.services.expression_service import ExpressionService from src.ai.services.question_service import QuestionService +from src.ai.services.report_service import ReportService from .conftest import MockProvider @@ -70,13 +72,15 @@ class TestQuestionService: async def test_generate_success(self) -> None: """生成题目成功.""" - output = json.dumps({ - "question": "1+1等于几?", - "answer": "2", - "explanation": "基础加法", - "difficulty": "easy", - "question_type": "short_answer", - }) + output = json.dumps( + { + "question": "1+1等于几?", + "answer": "2", + "explanation": "基础加法", + "difficulty": "easy", + "question_type": "short_answer", + } + ) chain = _make_chain(MockProvider(response_content=output)) gate = QualityGate(rule_validator=RuleValidator()) svc = QuestionService( @@ -150,10 +154,12 @@ class TestExpressionService: async def test_optimize_success(self) -> None: """优化成功.""" - output = json.dumps({ - "optimized": "优化后的文字", - "suggestions": ["建议1"], - }) + output = json.dumps( + { + "optimized": "优化后的文字", + "suggestions": ["建议1"], + } + ) chain = _make_chain(MockProvider(response_content=output)) svc = ExpressionService(failover_chain=chain) data = await svc.optimize(text="原始文字") @@ -192,3 +198,133 @@ class TestExpressionService: prompt = svc._fallback_prompt("文字", "上下文") assert "文字" in prompt assert "上下文" in prompt + + +class TestReportService: + """ReportService 测试.""" + + async def test_generate_class_summary_success(self) -> None: + """班级学情总结报告生成成功.""" + chain = _make_chain( + MockProvider( + response_content=( + "# 班级学情报告\n\n" + "## 摘要\n班级平均分 78.5,及格率 85%。\n\n" + "## 详细分析\n整体表现良好。\n\n" + "## 教学建议\n- 加强函数概念\n- 增加练习题\n" + ), + ) + ) + svc = ReportService( + failover_chain=chain, + data_ana_client=DataAnaClientMock(), + default_model="gpt-4o-mini", + ) + data = await svc.generate( + class_id="c-1", + report_type="class_summary", + ) + assert data.degraded is False + assert "班级学情报告" in data.content + assert data.summary # 非空 + assert len(data.recommendations) == 2 + assert "函数概念" in data.recommendations[0] + + async def test_generate_student_detail_with_student_id(self) -> None: + """学生详情报告(带 student_id).""" + chain = _make_chain( + MockProvider( + response_content=( + "# 学生学情详情\n\n" + "## 摘要\n该生在函数概念上较薄弱。\n\n" + "## 教学建议\n- 针对性练习\n" + ), + ) + ) + svc = ReportService( + failover_chain=chain, + data_ana_client=DataAnaClientMock(), + ) + data = await svc.generate( + class_id="c-1", + report_type="student_detail", + student_id="s-001", + ) + assert data.degraded is False + assert "学生学情详情" in data.content + + async def test_generate_degraded_llm_fail(self) -> None: + """LLM 不可用时降级.""" + chain = _make_chain(MockProvider(fail=True)) + svc = ReportService( + failover_chain=chain, + data_ana_client=DataAnaClientMock(), + ) + data = await svc.generate( + class_id="c-1", + report_type="exam_analysis", + ) + assert data.degraded is True + assert "LLM unavailable" in data.degraded_reason + assert data.content == "" + + async def test_generate_degraded_no_data_ana_client(self) -> None: + """无 data-ana 客户端时上下文降级(但 LLM 仍可生成).""" + chain = _make_chain(MockProvider(response_content="# 报告\n\n## 摘要\n无数据。")) + svc = ReportService( + failover_chain=chain, + data_ana_client=None, + ) + data = await svc.generate( + class_id="c-1", + report_type="class_summary", + ) + # LLM 可用 → 报告生成成功(上下文降级但不影响 LLM 调用) + assert data.degraded is False + assert "报告" in data.content + + def test_extract_summary_from_explicit_section(self) -> None: + """从「摘要」段落提取摘要.""" + chain = _make_chain(MockProvider()) + svc = ReportService(failover_chain=chain) + content = "# 报告\n\n## 摘要\n这是摘要内容。\n\n## 详细\n详情" + summary = svc._extract_summary(content) + assert "这是摘要内容" in summary + + def test_extract_summary_fallback_first_200_chars(self) -> None: + """无「摘要」段落时取前 200 字.""" + chain = _make_chain(MockProvider()) + svc = ReportService(failover_chain=chain) + content = "这是一段没有摘要标题的报告内容。" + summary = svc._extract_summary(content) + assert summary == content + + def test_extract_recommendations_from_dash_list(self) -> None: + """从「-」列表提取建议.""" + chain = _make_chain(MockProvider()) + svc = ReportService(failover_chain=chain) + content = "## 教学建议\n- 建议一\n- 建议二\n## 其他\n" + recs = svc._extract_recommendations(content) + assert recs == ["建议一", "建议二"] + + def test_extract_recommendations_from_numbered_list(self) -> None: + """从数字列表提取建议.""" + chain = _make_chain(MockProvider()) + svc = ReportService(failover_chain=chain) + content = "## 建议\n1. 第一条\n2. 第二条\n" + recs = svc._extract_recommendations(content) + assert recs == ["第一条", "第二条"] + + def test_build_prompt_with_context(self) -> None: + """构建 prompt 包含上下文数据.""" + chain = _make_chain(MockProvider()) + svc = ReportService(failover_chain=chain) + context = { + "class_id": "c-1", + "report_type": "class_summary", + "average_score": 78.5, + "pass_rate": 0.85, + } + prompt = svc._build_prompt("class_summary", context) + assert "class_summary" in prompt or "班级学情总结" in prompt + assert "78.5" in prompt