+ {{ section.title }} +
+
+{{ section.content }}
+
+ diff --git a/jiao-an-ai/README.md b/jiao-an-ai/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e512ec5a0344a317173748633a2e72dcee9167f1 --- /dev/null +++ b/jiao-an-ai/README.md @@ -0,0 +1,271 @@ +# jiao-an-ai:基于 LazyLLM 的智能教案生成项目 + +## 1. 项目简介 + +`jiao-an-ai` 是一个基于 **LazyLLM** 和 **FastAPI** 搭建的「AI 智能教案」独立项目,目标是帮助一线教师显著降低备课成本。你可以把它当作一个单独的 Web 服务来部署,`lazyllm` 仅作为普通依赖使用。 + +项目聚焦以下几点: + +- 老师只需输入课程名称、学科、年级、课型等基本信息,以及少量教学设想; +- 系统基于大模型自动生成一份结构化教案(包含教学目标、重难点、教学流程、差异化教学、评估与作业等模块); +- 支持 Web 表单交互和 REST API 两种使用方式,方便后续扩展为完整教案工作台。 + +当前版本重点是跑通整体链路:**表单输入 → LazyLLM 调用在线模型 → 解析结构化结果 → 页面展示教案**。 + +--- + +## 2. 技术栈与整体架构 + +### 2.1 后端 + +- `Python 3.10–3.12`(推荐 3.11) +- `FastAPI`:提供 HTTP API 和简单页面渲染 +- `Uvicorn[standard]`:ASGI 服务器 +- `Pydantic v2`:请求 / 响应模型定义 +- `LazyLLM`:大模型调用与 Agent 编排框架(通过 pip 安装为依赖) + - 使用 `OnlineChatModule` 统一封装在线大模型(OpenAI、通义千问、豆包等) +- (可选)后续可接入数据库(如 SQLite + SQLAlchemy)用于教案持久化 + +### 2.2 前端 + +- 基于 FastAPI 的 Jinja2 模板渲染 +- 简单的 HTML + CSS(`app/static/css/style.css`) +- 阶段 1:服务端渲染(SSR)表单 + 教案预览页面 +- 阶段 2(可选):可在此基础上引入 React/Vue,直接调用后端 JSON API + +### 2.3 目录结构 + +项目根目录为 `jiao-an-ai/`: + +```text +jiao-an-ai/ + README.md 本说明文档 + app/ + main.py FastAPI 入口 + __init__.py + api/ REST API 路由 + __init__.py + health.py 健康检查接口 + lesson_plans.py 教案生成 API 接口 + views/ 页面路由(Jinja2 模板渲染) + __init__.py + pages.py 首页 / 新建教案 / 预览页面路由 + models/ Pydantic 模型 + __init__.py + lesson_plan.py 教案请求 / 响应数据结构 + services/ 业务服务层 + __init__.py + lesson_plan_service.py 教案生成服务封装 + agents/ 基于 LazyLLM 的 Agent 封装 + __init__.py + lesson_plan_agent.py 教案生成 Agent(调用大模型) + templates/ Jinja2 HTML 模板 + index.html 首页 + lesson_plans_new.html 新建教案表单页面 + lesson_plans_detail.html 教案预览页面 + static/ + css/ + style.css 全局样式 + config/ + __init__.py 模型配置(统一创建 OnlineChatModule 实例) + scripts/ + __init__.py 预留脚本目录(运行 / 初始化脚本) +``` + +--- + +## 3. 功能概览 + +### 3.1 教案生成核心流程 + +1. 教师在前端表单中填写课程信息: + - 课程名称(例如:初一语文《背影》) + - 学科、年级、课型 + - 课时(分钟) + - 教学目标、教学重点、教学难点、特殊要求(可选) +2. 后端根据输入构造提示词(Prompt),明确要求大模型以 **JSON 数组** 的形式输出教案各模块: + - 每个元素形如:`{"title": "模块标题", "content": "详细内容"}` + - 建议包含:教学目标、教学重难点、教学流程、差异化教学、评估与作业等模块 +3. 使用 LazyLLM 的 `OnlineChatModule` 调用线上大模型,得到原始字符串结果; +4. 尝试将模型输出解析为 JSON,并映射为内部的 `LessonPlanSection` 列表; +5. 将结构化教案在网页上按模块展示,便于教师查看和后续微调。 + +### 3.2 提供的页面与接口 + +#### 页面 + +- `GET /` + - 首页,展示项目简介和「创建新教案」入口。 +- `GET /lesson-plans/new` + - 新建教案表单页面。 +- `POST /lesson-plans/preview` + - 提交表单并渲染教案预览页面。 + +#### REST API + +- `GET /api/health` + - 健康检查接口,返回 `{"status": "ok"}`。 +- `POST /api/lesson-plans/generate` + - 请求体:`LessonPlanRequest`(JSON) + - 返回体:`LessonPlanResponse`,包含 `id`、`sections`、`meta`。 + - 方便后续前后端分离或第三方系统集成。 + +--- + +## 4. 环境与依赖 + +### 4.1 Python 与虚拟环境 + +- Python 版本:`>=3.10, <3.13`(建议 3.11) +- 强烈建议使用虚拟环境(如 `python -m venv venv`)隔离依赖: + +```bash +cd jiao-an-ai +python -m venv venv +venv\Scripts\activate +``` + +### 4.2 安装依赖 + +`jiao-an-ai` 作为一个独立项目运行时,仅需要通过 pip 安装必要依赖即可,例如: + +```bash +pip install "fastapi[standard]" uvicorn lazyllm pydantic +``` + +如需精细控制版本,可以在项目根目录维护自己的 `pyproject.toml` 或 `requirements.txt`,将 `lazyllm`、`fastapi`、`uvicorn`、`pydantic` 等依赖写入其中,然后: + +```bash +pip install -r requirements.txt +``` + +### 4.3 配置在线大模型 + +项目通过 LazyLLM 的 `OnlineChatModule` 调用在线大模型,配置入口在: + +- `jiao-an-ai/config/__init__.py` + +环境变量说明: + +- `JIAO_AN_AI_LLM_SOURCE` + - 大模型来源平台,默认为 `"openai"`。 + - 可根据 LazyLLM 支持的 source 进行设置,例如:`openai` / `qwen` / `doubao` / `kimi` / `glm` / `sensenova` 等。 +- `JIAO_AN_AI_LLM_MODEL` + - 使用的具体模型名称,可选。 + - 当未设置时,使用对应平台的默认模型。 + +除此之外,你还需要根据所选平台配置相应的 API Key(例如 `OPENAI_API_KEY` 等),具体环境变量名称请参考 LazyLLM 官方文档和各云厂商文档。 +**注意:不要在代码仓库中硬编码或打印任何 API Key 等敏感信息。** + +--- + +## 5. 启动与使用 + +### 5.1 启动步骤 + +1. 进入 `jiao-an-ai` 项目根目录并激活虚拟环境: + +```bash +cd path\to\jiao-an-ai +venv\Scripts\activate +``` + +2. 确保 `lazyllm`、`fastapi`、`uvicorn` 等依赖已安装(见上文)。 + +3. 配置大模型环境变量(示例,以 OpenAI 为例): + +```bash +set JIAO_AN_AI_LLM_SOURCE=openai +set JIAO_AN_AI_LLM_MODEL=gpt-4o-mini +set OPENAI_API_KEY=你的Key +``` + +4. 启动开发服务: + +```bash +py -m uvicorn app.main:app --reload --host 127.0.0.1 --port 8000 +``` + +5. 在浏览器中访问: + +- 首页:`http://127.0.0.1:8000/` +- 新建教案:`http://127.0.0.1:8000/lesson-plans/new` + +### 5.2 示例表单填写建议 + +以语文课《背影》为例,可以这样填写: + +- 课程名称:`初一语文《背影》` +- 学科:`语文` +- 年级:`初一` +- 课型:`新授课` +- 课时:`45` +- 教学目标: + - `帮助学生理解文章中父爱的含蓄表达,学会从细节体会人物情感。` +- 教学重点: + - `抓住描写父亲“背影”的细节,体会情感。` +- 教学难点: + - `引导学生联系自身生活经验,进行情感共鸣。` +- 特殊要求: + - `课堂中至少安排一次小组讨论,鼓励学生主动发言。` + +提交后,系统会调用大模型生成结构化教案,并在预览页中按模块显示。 + +--- + +## 6. 核心模块说明 + +### 6.1 模型配置:`config/__init__.py` + +- `get_chat_module()`:使用 `OnlineChatModule` 封装在线大模型,并通过 `@lru_cache` 进行复用。 +- 支持通过环境变量动态切换大模型来源与型号,方便后续对比不同模型效果。 + +### 6.2 教案 Agent:`app/agents/lesson_plan_agent.py` + +- 核心职责: + - 将教师输入转换为清晰的中文提示词; + - 要求模型输出结构化 JSON; + - 解析 JSON 并转换为内部 `LessonPlanSection`。 +- 异常处理: + - 如果模型输出不能被正确解析为 JSON,返回一个包含基本信息和错误提示的保底教案,避免前端崩溃。 + +### 6.3 服务层:`app/services/lesson_plan_service.py` + +- `generate_lesson_plan`: + - 封装教案生成的业务流程; + - 负责生成教案 ID、填充 meta 信息; + - 是 API 层与 Agent 的中间层,后续可在此添加持久化、日志等能力。 + +### 6.4 API 与视图:`app/api` 与 `app/views` + +- API 层: + - `health.py`:提供 `/api/health` 健康检查接口。 + - `lesson_plans.py`:提供 `/api/lesson-plans/generate` 教案生成接口。 +- 视图层: + - `pages.py`: + - `/`:首页; + - `/lesson-plans/new`:新建教案表单; + - `/lesson-plans/preview`:表单提交后预览教案。 + +--- + +## 7. 后续规划与扩展方向 + +当前项目处于原型阶段,后续可以在此基础上扩展: + +1. **教案模板多样化** + - 按学科、学段、课型配置不同的提示词与模块结构; + - 支持学校自定义模板。 +2. **教案版本管理与持久化** + - 引入数据库(SQLite / MySQL / PostgreSQL)存储教案; + - 支持版本回溯、复制、基于历史教案二次生成。 +3. **差异化教学与分层作业** + - 明确学困生 / 资优生 / 特殊需要学生的教学策略; + - 生成分层作业与课堂即时检测题库。 +4. **与现有教务系统集成** + - 提供稳定的 REST API 或导出格式(如 Word / PDF / Markdown),便于导入学校教案系统。 +5. **效果评估与优化** + - 收集老师对生成教案的修改数据,反向优化提示词与工作流; + - 逐步从单模型调用演进到多 Agent 协作(如目标规划 Agent、活动设计 Agent、评估设计 Agent 等)。 + + diff --git a/jiao-an-ai/app/__init__.py b/jiao-an-ai/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/jiao-an-ai/app/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..042bb2b0ec6c863067ba449cdaaa1b739924c28f Binary files /dev/null and b/jiao-an-ai/app/__pycache__/__init__.cpython-311.pyc differ diff --git a/jiao-an-ai/app/__pycache__/main.cpython-311.pyc b/jiao-an-ai/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a30fd2fb142f553c9d78b22bb89fb651dd4871b7 Binary files /dev/null and b/jiao-an-ai/app/__pycache__/main.cpython-311.pyc differ diff --git a/jiao-an-ai/app/agents/__init__.py b/jiao-an-ai/app/agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/jiao-an-ai/app/agents/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/agents/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/agents/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f5b7503cea54ee080f7b5cb50dd68a065850b1d Binary files /dev/null and b/jiao-an-ai/app/agents/__pycache__/__init__.cpython-311.pyc differ diff --git a/jiao-an-ai/app/agents/__pycache__/lesson_plan_agent.cpython-311.pyc b/jiao-an-ai/app/agents/__pycache__/lesson_plan_agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d8818709b0d4958f14b6f678e541e3611cbb2c9 Binary files /dev/null and b/jiao-an-ai/app/agents/__pycache__/lesson_plan_agent.cpython-311.pyc differ diff --git a/jiao-an-ai/app/agents/lesson_plan_agent.py b/jiao-an-ai/app/agents/lesson_plan_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..c839f690f42a246b8582c75da6b3fd534401cb0e --- /dev/null +++ b/jiao-an-ai/app/agents/lesson_plan_agent.py @@ -0,0 +1,364 @@ +import json +from typing import Any, Iterable + +import lazyllm + +from app.models.lesson_plan import ( + LessonPlanRequest, + LessonPlanReviewResponse, + LessonPlanSection, + LessonPlanVariant, +) +from config import get_chat_module + + +def _build_prompt(data: LessonPlanRequest, profile: str | None = None) -> str: + """ + 构造教案生成提示词 + """ + base_info = ( + f"课程名称:{data.course_name}\n" + f"学科:{data.subject}\n" + f"年级:{data.grade}\n" + f"课型:{data.lesson_type}\n" + f"课时:{data.duration_minutes} 分钟\n" + ) + goals = data.teaching_goals or "" + key_points = data.key_points or "" + difficult_points = data.difficult_points or "" + requirements = data.special_requirements or "" + profile_hint = "" + if profile == "low": + profile_hint = ( + "当前班级整体基础较弱,部分学生学习信心不足,需要更多示例讲解、课堂练习和过程性支撑。\n" + "请在设计中增加铺垫、拆解步骤、同伴互助等环节,降低一次性认知负担。\n" + ) + elif profile == "high": + profile_hint = ( + "当前班级整体基础较好,学习能力较强,适合增加思维挑战和拓展任务。\n" + "请在设计中加入探究式活动、开放性问题和拓展阅读等内容,避免重复性机械练习。\n" + ) + elif profile == "normal": + profile_hint = ( + "当前班级学情整体均衡,既要保证基础知识落实,也要保留适当的思维提升与讨论空间。\n" + ) + extra_profile = "" + if profile_hint: + extra_profile = "【班级学情提示】\n" + profile_hint + return ( + "你是一名熟悉中国义务教育课程标准的资深教研员,请根据以下课程信息," + "生成一份教案的结构化框架。只需要输出 JSON,不要输出任何额外说明。\n\n" + "【课程信息】\n" + f"{base_info}" + f"教师给出的初步教学目标:{goals}\n" + f"教师给出的教学重点:{key_points}\n" + f"教师给出的教学难点:{difficult_points}\n" + f"教师的特殊要求:{requirements}\n" + f"{extra_profile}" + "【输出要求】\n" + "1. 严格输出一个 JSON 数组,每个元素为一个对象,形如:\n" + ' {"title": "模块标题", "content": "详细内容"}\n' + "2. 建议包含以下模块:教学目标、教学重难点、教学流程、差异化教学、评估与作业。\n" + "3. content 字段为字符串,可以包含换行。\n" + "4. 不要在 JSON 之外输出任何文字,例如解释、前后缀等。\n" + ) + + +def _build_stream_prompt(data: LessonPlanRequest) -> str: + """ + 构造用于流式 Markdown 预览的提示词 + """ + base_info = ( + f"课程名称:{data.course_name}\n" + f"学科:{data.subject}\n" + f"年级:{data.grade}\n" + f"课型:{data.lesson_type}\n" + f"课时:{data.duration_minutes} 分钟\n" + ) + goals = data.teaching_goals or "" + key_points = data.key_points or "" + difficult_points = data.difficult_points or "" + requirements = data.special_requirements or "" + return ( + "你是一名熟悉中国义务教育课程标准的资深教研员,请根据以下课程信息," + "生成一份可以直接用于上课的详细教案草案,输出格式为 Markdown。\n\n" + "【课程信息】\n" + f"{base_info}" + f"教师给出的初步教学目标:{goals}\n" + f"教师给出的教学重点:{key_points}\n" + f"教师给出的教学难点:{difficult_points}\n" + f"教师的特殊要求:{requirements}\n\n" + "【输出要求】\n" + "1. 只输出 Markdown 文本,不要输出 JSON、XML 或代码块标记。\n" + "2. 使用合适的标题层级组织内容,建议包含:教学目标、教学重难点、教学流程、差异化教学、课堂评价与作业等模块。\n" + "3. 内容条理清晰、段落分明,适合教师直接复制和微调。\n" + ) + + +def _build_review_prompt(data: LessonPlanRequest) -> str: + """ + 构造教案评审与打分提示词 + """ + base_info = ( + f"课程名称:{data.course_name}\n" + f"学科:{data.subject}\n" + f"年级:{data.grade}\n" + f"课型:{data.lesson_type}\n" + f"课时:{data.duration_minutes} 分钟\n" + ) + goals = data.teaching_goals or "" + key_points = data.key_points or "" + difficult_points = data.difficult_points or "" + requirements = data.special_requirements or "" + profiles = ", ".join(data.class_profiles or []) or "默认班级" + return ( + "你是一名熟悉中国义务教育课程标准的一线教研员,请基于下面这节课的整体设计意图," + "从教学目标匹配度、重难点落实、学情适配、课堂活动设计、评价与作业等维度做一次专业评审,并给出 0-100 分的综合得分。\n\n" + "【课程信息】\n" + f"{base_info}" + f"教师给出的初步教学目标:{goals}\n" + f"教师给出的教学重点:{key_points}\n" + f"教师给出的教学难点:{difficult_points}\n" + f"教师的特殊要求:{requirements}\n" + f"目标适配班级学情:{profiles}\n\n" + "【评审输出要求】\n" + "1. 请只返回一个 JSON 对象,不要输出任何额外解释或前后缀。\n" + "2. JSON 字段说明如下(字段名需保持一致):score、level、summary、strengths、improvements。\n" + "3. 其中 score 为 0-100 的整数,level 为“优秀/良好/合格/待优化”等等级描述," + "summary 为总体评价概述,strengths 为若干条亮点(可使用换行分隔)," + "improvements 为若干条有针对性的改进建议(可使用换行分隔)。\n" + ) + + +def _extract_json_text(raw: str) -> str | None: + """ + 从大模型原始输出中尽量提取可解析的 JSON 文本 + """ + text = (raw or "").strip() + if not text: + return None + + if text.startswith("```"): + lines = text.splitlines() + if lines and lines[0].startswith("```"): + lines = lines[1:] + if lines and lines[-1].startswith("```"): + lines = lines[:-1] + text = "\n".join(lines).strip() + + try: + json.loads(text) + return text + except Exception: + pass + + start = text.find("[") + end = text.rfind("]") + if start != -1 and end != -1 and end > start: + candidate = text[start : end + 1] + try: + json.loads(candidate) + return candidate + except Exception: + pass + + start = text.find("{") + end = text.rfind("}") + if start != -1 and end != -1 and end > start: + candidate = text[start : end + 1] + try: + obj = json.loads(candidate) + if isinstance(obj, dict): + return json.dumps([obj], ensure_ascii=False) + except Exception: + pass + + return None + + +def _generate_sections_from_prompt(prompt: str, data: LessonPlanRequest) -> list[LessonPlanSection]: + """ + 使用指定提示词调用大模型并解析为分节内容 + """ + try: + chat = get_chat_module() + raw = chat(prompt) + except Exception as exc: + raw = str(exc) + + try: + json_text = _extract_json_text(raw) + if json_text is None: + raise ValueError("no valid json text") + parsed: Any = json.loads(json_text) + sections: list[LessonPlanSection] = [] + if isinstance(parsed, list): + for item in parsed: + if not isinstance(item, dict): + continue + title = str(item.get("title", "")).strip() + content = str(item.get("content", "")).strip() + if not title and not content: + continue + sections.append(LessonPlanSection(title=title, content=content)) + if sections: + return sections + except Exception: + pass + + raw_text = (raw or "").strip() + if raw_text: + return [LessonPlanSection(title="AI 生成教案草案", content=raw_text)] + + overview = LessonPlanSection( + title="教学概述", + content=( + f"课程名称:{data.course_name};学科:{data.subject};" + f"年级:{data.grade};课型:{data.lesson_type};" + f"课时:{data.duration_minutes} 分钟。" + ), + ) + fallback_content = "AI 教案生成出现问题,请稍后重试或检查大模型配置。" + return [overview, LessonPlanSection(title="教案内容", content=fallback_content)] + + +def generate_lesson_plan_sections(data: LessonPlanRequest) -> list[LessonPlanSection]: + """ + 基于 LazyLLM 生成教案分节内容 + """ + prompt = _build_prompt(data) + return _generate_sections_from_prompt(prompt, data) + + +def _normalize_profiles(class_profiles: list[str] | None) -> list[str]: + """ + 归一化班级画像列表 + """ + if not class_profiles: + return ["normal"] + normalized: list[str] = [] + for p in class_profiles: + p = (p or "").strip().lower() + if not p: + continue + if p in {"low", "normal", "high"}: + normalized.append(p) + elif "困" in p or "差" in p: + normalized.append("low") + elif "优" in p or "拔高" in p: + normalized.append("high") + else: + normalized.append("normal") + if not normalized: + normalized.append("normal") + return list(dict.fromkeys(normalized)) + + +def _profile_label(profile: str) -> str: + """ + 将班级画像编码转换为展示标签 + """ + if profile == "low": + return "学困型班级教案" + if profile == "high": + return "拔高型班级教案" + return "标准班级教案" + + +def generate_lesson_plan_variants(data: LessonPlanRequest) -> list[LessonPlanVariant]: + """ + 生成按班级学情区分的多版本教案 + """ + profiles = _normalize_profiles(data.class_profiles) + variants: list[LessonPlanVariant] = [] + for profile in profiles: + prompt = _build_prompt(data, profile=profile) + sections = _generate_sections_from_prompt(prompt, data) + variants.append( + LessonPlanVariant( + profile=profile, + label=_profile_label(profile), + sections=sections, + ) + ) + return variants + + +def stream_lesson_plan_text(data: LessonPlanRequest) -> Iterable[str]: + """ + 流式生成教案文本内容 + """ + prompt = _build_stream_prompt(data) + chat = get_chat_module() + lazyllm.FileSystemQueue().clear() + queue = lazyllm.FileSystemQueue() + + with lazyllm.ThreadPoolExecutor(1) as executor: + future = executor.submit(chat, prompt) + while True: + value = queue.dequeue() + if value: + chunk = "".join(value) + if chunk: + yield chunk + elif future.done(): + break + _ = future.result() + lazyllm.FileSystemQueue().clear() + + +def review_lesson_plan(data: LessonPlanRequest) -> LessonPlanReviewResponse: + """ + 基于 LazyLLM 对教案进行评审与打分 + """ + prompt = _build_review_prompt(data) + try: + chat = get_chat_module() + raw = chat(prompt) + except Exception as exc: + raw = str(exc) + + text = (raw or "").strip() + if not text: + return LessonPlanReviewResponse( + score=0, + level="待优化", + summary="AI 评审出现问题,请稍后重试或检查大模型配置。", + strengths=None, + improvements=None, + ) + + try: + json_text = _extract_json_text(text) or text + parsed: Any = json.loads(json_text) + if isinstance(parsed, list) and parsed: + parsed = parsed[0] + if isinstance(parsed, dict): + score_raw = parsed.get("score", 0) + try: + score = int(score_raw) + except Exception: + score = 0 + level = str(parsed.get("level") or "").strip() or "待优化" + summary = str(parsed.get("summary") or "").strip() + strengths = str(parsed.get("strengths") or "").strip() + improvements = str(parsed.get("improvements") or "").strip() + if not summary: + summary = "AI 已完成评审,但未返回详细说明。" + return LessonPlanReviewResponse( + score=score, + level=level, + summary=summary, + strengths=strengths or None, + improvements=improvements or None, + ) + except Exception: + pass + + return LessonPlanReviewResponse( + score=0, + level="待优化", + summary=text, + strengths=None, + improvements=None, + ) diff --git a/jiao-an-ai/app/api/__init__.py b/jiao-an-ai/app/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/jiao-an-ai/app/api/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/api/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4143002dfde506b48cc8760df1c7f2d66ad5ba5 Binary files /dev/null and b/jiao-an-ai/app/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/jiao-an-ai/app/api/__pycache__/health.cpython-311.pyc b/jiao-an-ai/app/api/__pycache__/health.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13dd484e4334a5dbf14d6f98e83846433ced879b Binary files /dev/null and b/jiao-an-ai/app/api/__pycache__/health.cpython-311.pyc differ diff --git a/jiao-an-ai/app/api/__pycache__/lesson_plans.cpython-311.pyc b/jiao-an-ai/app/api/__pycache__/lesson_plans.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..141abad7c43a4d2d2dda3425f5ec52034b27ade7 Binary files /dev/null and b/jiao-an-ai/app/api/__pycache__/lesson_plans.cpython-311.pyc differ diff --git a/jiao-an-ai/app/api/health.py b/jiao-an-ai/app/api/health.py new file mode 100644 index 0000000000000000000000000000000000000000..e5e50fc7d0c2e02bba5b84c3315426eca81730ec --- /dev/null +++ b/jiao-an-ai/app/api/health.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + + +router = APIRouter(prefix="/api") + + +@router.get("/health") +async def health_check() -> dict: + """ + 健康检查接口 + """ + return {"status": "ok"} + diff --git a/jiao-an-ai/app/api/lesson_plans.py b/jiao-an-ai/app/api/lesson_plans.py new file mode 100644 index 0000000000000000000000000000000000000000..d02475c0b8c2d75902ccb98719ffae1ea3b898ca --- /dev/null +++ b/jiao-an-ai/app/api/lesson_plans.py @@ -0,0 +1,43 @@ +from fastapi import APIRouter +from fastapi.responses import StreamingResponse + +from app.models.lesson_plan import ( + LessonPlanRequest, + LessonPlanResponse, + LessonPlanReviewResponse, +) +from app.services.lesson_plan_service import ( + generate_lesson_plan, + review_lesson_plan, + stream_lesson_plan, +) + + +router = APIRouter(prefix="/api/lesson-plans", tags=["lesson_plans"]) + + +@router.post("/generate", response_model=LessonPlanResponse) +async def generate_lesson_plan_api(data: LessonPlanRequest) -> LessonPlanResponse: + """ + 教案生成接口 + """ + return generate_lesson_plan(data) + + +@router.post("/generate-stream") +async def generate_lesson_plan_stream_api(data: LessonPlanRequest) -> StreamingResponse: + """ + 流式教案生成接口 + """ + return StreamingResponse( + stream_lesson_plan(data), + media_type="text/plain; charset=utf-8", + ) + + +@router.post("/review", response_model=LessonPlanReviewResponse) +async def review_lesson_plan_api(data: LessonPlanRequest) -> LessonPlanReviewResponse: + """ + 教案评审与打分接口 + """ + return review_lesson_plan(data) diff --git a/jiao-an-ai/app/main.py b/jiao-an-ai/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..65d5f265c28b7bb87ee3a3461910129e648529f6 --- /dev/null +++ b/jiao-an-ai/app/main.py @@ -0,0 +1,36 @@ +from pathlib import Path + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from app.api.health import router as health_router +from app.api.lesson_plans import router as lesson_plans_router +from app.views.pages import router as pages_router + + +BASE_DIR = Path(__file__).resolve().parent +STATIC_DIR = BASE_DIR / "static" +TEMPLATES_DIR = BASE_DIR / "templates" + + +def create_app() -> FastAPI: + """ + 创建 FastAPI 应用实例 + """ + app = FastAPI() + app.include_router(health_router) + app.include_router(lesson_plans_router) + app.include_router(pages_router) + app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") + Jinja2Templates(directory=str(TEMPLATES_DIR)) + return app + + +app = create_app() + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) diff --git a/jiao-an-ai/app/models/__init__.py b/jiao-an-ai/app/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/jiao-an-ai/app/models/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/models/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb37deb656d7a2799c00c88045447b651de195ab Binary files /dev/null and b/jiao-an-ai/app/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/jiao-an-ai/app/models/__pycache__/lesson_plan.cpython-311.pyc b/jiao-an-ai/app/models/__pycache__/lesson_plan.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3276ca8ed84f038d3fc0fad0e3a9beadf45632d6 Binary files /dev/null and b/jiao-an-ai/app/models/__pycache__/lesson_plan.cpython-311.pyc differ diff --git a/jiao-an-ai/app/models/lesson_plan.py b/jiao-an-ai/app/models/lesson_plan.py new file mode 100644 index 0000000000000000000000000000000000000000..689e36ddc2fa469f8e3ea4afa260d7cea51034c9 --- /dev/null +++ b/jiao-an-ai/app/models/lesson_plan.py @@ -0,0 +1,60 @@ +from pydantic import BaseModel + + +class LessonPlanRequest(BaseModel): + """ + 教案生成请求模型 + """ + + course_name: str + subject: str + grade: str + lesson_type: str + duration_minutes: int + teaching_goals: str | None = None + key_points: str | None = None + difficult_points: str | None = None + special_requirements: str | None = None + class_profiles: list[str] | None = None + + +class LessonPlanSection(BaseModel): + """ + 教案中的分节内容 + """ + + title: str + content: str + + +class LessonPlanVariant(BaseModel): + """ + 按班级学情区分的教案版本 + """ + + profile: str + label: str + sections: list[LessonPlanSection] + + +class LessonPlanResponse(BaseModel): + """ + 教案生成响应模型 + """ + + id: str + sections: list[LessonPlanSection] + meta: dict + variants: list[LessonPlanVariant] | None = None + + +class LessonPlanReviewResponse(BaseModel): + """ + 教案评审与打分结果模型 + """ + + score: int + level: str + summary: str + strengths: str | None = None + improvements: str | None = None diff --git a/jiao-an-ai/app/services/__init__.py b/jiao-an-ai/app/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/jiao-an-ai/app/services/__init__.py @@ -0,0 +1 @@ + diff --git a/jiao-an-ai/app/services/__pycache__/__init__.cpython-311.pyc b/jiao-an-ai/app/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..be1fffbea62d787f134e657c1b1e83f3caa68ac5 Binary files /dev/null and b/jiao-an-ai/app/services/__pycache__/__init__.cpython-311.pyc differ diff --git a/jiao-an-ai/app/services/__pycache__/lesson_plan_service.cpython-311.pyc b/jiao-an-ai/app/services/__pycache__/lesson_plan_service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..479dd353545295e0d1de4f9e9823b145dc523297 Binary files /dev/null and b/jiao-an-ai/app/services/__pycache__/lesson_plan_service.cpython-311.pyc differ diff --git a/jiao-an-ai/app/services/lesson_plan_service.py b/jiao-an-ai/app/services/lesson_plan_service.py new file mode 100644 index 0000000000000000000000000000000000000000..cdd02de52cd9a56e4627552db69e7ee9324822d8 --- /dev/null +++ b/jiao-an-ai/app/services/lesson_plan_service.py @@ -0,0 +1,52 @@ +import uuid + +from typing import Iterable + +from app.agents.lesson_plan_agent import ( + generate_lesson_plan_sections, + generate_lesson_plan_variants, + review_lesson_plan as review_lesson_plan_agent, + stream_lesson_plan_text, +) +from app.models.lesson_plan import ( + LessonPlanRequest, + LessonPlanResponse, + LessonPlanReviewResponse, +) + + +def generate_lesson_plan(data: LessonPlanRequest) -> LessonPlanResponse: + """ + 生成教案响应对象 + """ + variants = None + if data.class_profiles: + variants = generate_lesson_plan_variants(data) + if variants: + sections = variants[0].sections + else: + sections = generate_lesson_plan_sections(data) + else: + sections = generate_lesson_plan_sections(data) + meta = { + "course_name": data.course_name, + "subject": data.subject, + "grade": data.grade, + "lesson_type": data.lesson_type, + "duration_minutes": data.duration_minutes, + } + return LessonPlanResponse(id=str(uuid.uuid4()), sections=sections, meta=meta, variants=variants) + + +def stream_lesson_plan(data: LessonPlanRequest) -> Iterable[str]: + """ + 流式生成教案原始文本 + """ + return stream_lesson_plan_text(data) + + +def review_lesson_plan(data: LessonPlanRequest) -> LessonPlanReviewResponse: + """ + 评审教案并返回打分结果 + """ + return review_lesson_plan_agent(data) diff --git a/jiao-an-ai/app/static/css/style.css b/jiao-an-ai/app/static/css/style.css new file mode 100644 index 0000000000000000000000000000000000000000..699a279c1b425c3a9aee024ce42ea2f749a49b11 --- /dev/null +++ b/jiao-an-ai/app/static/css/style.css @@ -0,0 +1,3 @@ +body { + margin: 0; +} diff --git a/jiao-an-ai/app/templates/index.html b/jiao-an-ai/app/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..887e475a04d987361566376e2a85cc33512b8922 --- /dev/null +++ b/jiao-an-ai/app/templates/index.html @@ -0,0 +1,335 @@ + + +
+ ++ + 每节课平均节省 60% 备课时间 +
++ AI 智能教案专注一件事:用最懂教学的方式,把你的想法快速整理成规范教案, + 帮你从搜索资料、排版格式中解放出来,把时间还给学生和课堂。 +
++ 按教学目标、重难点、教学流程、差异化、作业与评估等模块输出, + 贴合学校常用教案模板。 +
++ 你只需提供课程信息和大致思路,其余交给系统,让修改比从零写更轻松。 +
++ 支持设置课时长度和课型,生成的活动设计更容易直接搬到课堂上用。 +
++ 通过细读文本和课堂活动,引导学生理解文本情感, + 提升朗读表达与情感共鸣能力。 +
++ 重点:抓住关键细节体会人物情感。难点:帮助学生联系自身生活经验, + 把“理解父爱”变成学生自己的表达。 +
++ 情境导入 → 默读标记 → 小组讨论 → 全班交流 → 情感升华 → 课后延伸。 +
++ 我们只解决老师日常最真实的几个问题:时间不够、结构难想、课堂不好设计。 +
++ 教师专注于确定教学目标和课堂方向;查资料、拆结构、润色表述等机械工作交给系统完成, + 避免被 PPT 和格式牵着走。 +
++ 输出结构统一、逻辑清晰的教案,便于与备课组共享、对照修改, + 大幅减少“看不懂别人教案”的沟通成本。 +
++ 系统给出的是“80% 完成度”的草案,你可以根据班级特点随时调整活动设计和提问方式, + 不会变成千篇一律的流水线课堂。 +
++ 对教材内容不够熟悉时,先生成一份结构清晰的教案,帮助快速理清思路。 +
++ 用系统先跑出一个「教研版」教案,再和同事一起打磨亮点和细节。 +
++ 基于一份教案模板,快速调整目标和活动设计,生成适配不同班级的版本。 +
++ 通过简单说明班级情况,让系统自动给出学困生、资优生的不同任务建议。 +
++ 省下来的时间,可以用在找练习、批改作业、设计更有趣的课堂活动, + 而不是和文档模板“斗争”。 +
++ 下面是我们在打磨产品过程中,老师们最常给出的几句话反馈。 +
++ “以前写教案最怕打开空白 Word,现在只要先生成一个框架, + 我只需要改细节,反而更容易想清楚整节课怎么走。” +
++ “结构跟学校教案本子基本一致,复制过去稍微调一下就能交, + 不用再到处找模板抄格式了。” +
++ “我会把重要的课堂提问和活动改成自己的语言, + 但有这个底稿以后,备课不会再拖到半夜了。” +
++ 现在就试着用 AI 帮你准备下一节课的教案。先从一节课开始, + 感受一下“备课不再是熬夜”的状态。 +
++ 以下内容由 AI 根据你填写的信息生成,你可以直接复制到学校教案系统中并按需修改。 +
+
+{{ section.content }}
+
+ + 提示:你可以在教案系统中按模块粘贴或整体粘贴本页内容。 +
++ 建议尽量填写清晰的教学目标和重难点,AI 会在此基础上生成更贴合课堂的教案结构。 +
+ + + + ++ 生成过程会以 Markdown 形式实时渲染,便于直接阅读和复制。 +
++ 基于课程信息,从教学目标、学情适配等维度给出综合评价。 +
+