commit 3ebb249516da928f5633d385d6d973babd1724c2
Author: Cxx0822 <1556464090@qq.com>
Date: Mon Jul 27 20:00:57 2026 +0800
feat:初始化工程
diff --git a/.env b/.env
new file mode 100644
index 0000000..cb0341f
--- /dev/null
+++ b/.env
@@ -0,0 +1,12 @@
+RUSTFS_ENDPOINT=http://114.66.28.183:42736
+RUSTFS_ACCESS_KEY=B4K79q5HGhPJlnXfLs20
+RUSTFS_SECRET_KEY=j9b3M2LRU80okVcZnBOxePvd7NXF5TSrqf6gImA1
+RUSTFS_BUCKET=study
+
+OCR_API_URL=https://paddleocr.aistudio-app.com/api/v2/ocr/jobs
+OCR_API_TOKEN=df7dcc85a5c3c9d64e421f353d11d13ec45512f6
+OCR_MODEL=PaddleOCR-VL-1.6
+
+MODEL_NAME=deepseek-v4-flash
+MODEL_BASE_URL=https://api.deepseek.com
+MODEL_API_KEY=sk-0b237d41f6bc44fc9732ea66bd7eade0
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7ddac70
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,68 @@
+# Python 字节码文件
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C 扩展
+*.so
+
+# 分发/打包
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# 虚拟环境
+venv/
+env/
+ENV/
+.env
+.venv
+
+# 测试
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+
+# Django 相关
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+media/
+
+# PyCharm IDE
+.idea/
+*.iml
+*.iws
+*.ipr
+
+# VS Code
+.vscode/
+*.code-workspace
+.history/
+
+# 其他
+.DS_Store
+
+logs/
+packages/
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..366a79c
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/study-agent-service.iml b/.idea/study-agent-service.iml
new file mode 100644
index 0000000..694ec20
--- /dev/null
+++ b/.idea/study-agent-service.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..3f2507e
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1785141112222
+
+
+ 1785141112222
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/agent/model.py b/agent/model.py
new file mode 100644
index 0000000..0dd7587
--- /dev/null
+++ b/agent/model.py
@@ -0,0 +1,13 @@
+import os
+
+from dotenv import load_dotenv
+from langchain.chat_models import init_chat_model
+
+load_dotenv()
+
+model = init_chat_model(
+ model=os.getenv('MODEL_NAME'),
+ model_provider="openai",
+ base_url=os.getenv('MODEL_BASE_URL'),
+ api_key=os.getenv('MODEL_API_KEY')
+)
diff --git a/agent/prompt.py b/agent/prompt.py
new file mode 100644
index 0000000..d3c1629
--- /dev/null
+++ b/agent/prompt.py
@@ -0,0 +1,27 @@
+study_prompt = """
+你是一位专业的出题专家,擅长根据学习材料生成高质量选择题。
+
+要求:
+1. 题目必须严格基于学习内容,不得编造知识点。
+2. 每道题只有 1 个正确答案。
+3. 干扰项要有迷惑性。
+4. 输出必须是纯 JSON,禁止 Markdown。
+5. 不要修改专业术语。
+
+返回格式:
+{
+ "questions": [
+ {
+ "id": 1,
+ "question": "题目内容",
+ "options": {
+ "A": "选项A内容",
+ "B": "选项B内容",
+ "C": "选项C内容",
+ "D": "选项D内容"
+ },
+ "answer": "A"
+ }
+ ]
+}
+"""
\ No newline at end of file
diff --git a/agent/study.py b/agent/study.py
new file mode 100644
index 0000000..d21b9e9
--- /dev/null
+++ b/agent/study.py
@@ -0,0 +1,9 @@
+from langchain.agents import create_agent
+
+from agent.model import model
+from agent.prompt import study_prompt
+
+study_agent = create_agent(
+ model=model,
+ system_prompt=study_prompt
+)
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..ed31b4c
--- /dev/null
+++ b/main.py
@@ -0,0 +1,28 @@
+from fastapi import FastAPI, UploadFile, File
+from starlette.responses import StreamingResponse
+
+from models.agent import QueryRequest
+from service import query_agent
+from storage import upload_rustfs
+from ocr import ocr_from_url
+
+app = FastAPI(title="AI Study Service")
+
+
+@app.post("/upload")
+def upload(file: UploadFile = File(...)):
+ url = upload_rustfs(file.file, file.filename)
+ text = ocr_from_url(url)
+
+ return {
+ "text": text,
+ }
+
+
+@app.post("/generate")
+def generate(query: QueryRequest):
+ """流式对话"""
+ return StreamingResponse(
+ query_agent(query),
+ media_type="text/event-stream"
+ )
diff --git a/models/agent.py b/models/agent.py
new file mode 100644
index 0000000..4f39ab8
--- /dev/null
+++ b/models/agent.py
@@ -0,0 +1,5 @@
+from pydantic import BaseModel
+
+
+class QueryRequest(BaseModel):
+ message: str
diff --git a/ocr.py b/ocr.py
new file mode 100644
index 0000000..908e1f6
--- /dev/null
+++ b/ocr.py
@@ -0,0 +1,53 @@
+import os
+import time
+import requests
+from dotenv import load_dotenv
+
+load_dotenv()
+
+API_URL = os.getenv('OCR_API_URL')
+HEADERS = {
+ "Authorization": f"bearer {os.getenv('OCR_API_TOKEN')}",
+ "Content-Type": "application/json",
+}
+MODEL = os.getenv("OCR_MODEL")
+
+
+def ocr_from_url(image_url: str) -> str:
+ r = requests.post(
+ API_URL,
+ json={
+ "fileUrl": image_url,
+ "model": MODEL,
+ "optionalPayload": {
+ "useDocOrientationClassify": False,
+ "useDocUnwarping": False,
+ "useChartRecognition": False,
+ },
+ },
+ headers=HEADERS,
+ )
+ r.raise_for_status()
+ job_id = r.json()["data"]["jobId"]
+
+ while True:
+ r = requests.get(f"{API_URL}/{job_id}", headers=HEADERS)
+ r.raise_for_status()
+ data = r.json()["data"]
+
+ if data["state"] == "done":
+ jsonl_url = data["resultUrl"]["jsonUrl"]
+ break
+ if data["state"] == "failed":
+ raise RuntimeError("OCR failed")
+
+ time.sleep(3)
+
+ lines = requests.get(jsonl_url).text.strip().splitlines()
+ text = ""
+ for line in lines:
+ item = __import__("json").loads(line)
+ for res in item["result"]["layoutParsingResults"]:
+ text += res["markdown"]["text"]
+
+ return text
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..15db8cd
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,10 @@
+fastapi~=0.140.0
+python-dotenv~=1.2.2
+requests~=2.34.2
+boto3~=1.43.56
+botocore~=1.43.56
+langchain~=1.3.14
+langchain-core~=1.5.1
+langchain-openai~=1.4.1
+starlette~=1.3.1
+pydantic~=2.13.4
\ No newline at end of file
diff --git a/service.py b/service.py
new file mode 100644
index 0000000..230476f
--- /dev/null
+++ b/service.py
@@ -0,0 +1,21 @@
+from langchain_core.messages import AIMessageChunk, HumanMessage
+
+from agent.study import study_agent
+from models.agent import QueryRequest
+
+
+def query_agent(query: QueryRequest):
+ try:
+ user_msg = f"学习内容:\n{query.message}"
+
+ # 流式调用Agent
+ for chunk, metadata in study_agent.stream(
+ {"messages": [HumanMessage(content=user_msg)]},
+ stream_mode="messages"
+ ):
+ if isinstance(chunk, AIMessageChunk):
+ if isinstance(chunk, AIMessageChunk) and chunk.content:
+ yield chunk.content
+ except Exception as e:
+ print(f"\n[错误]: {str(e)}")
+ yield "信息检索失败,请重新输入问题提问"
diff --git a/storage.py b/storage.py
new file mode 100644
index 0000000..4ef253e
--- /dev/null
+++ b/storage.py
@@ -0,0 +1,24 @@
+import os
+import boto3
+from botocore.config import Config
+from dotenv import load_dotenv
+
+load_dotenv()
+
+ENDPOINT = os.getenv("RUSTFS_ENDPOINT")
+BUCKET = os.getenv("RUSTFS_BUCKET")
+
+_s3 = boto3.client(
+ "s3",
+ endpoint_url=ENDPOINT,
+ aws_access_key_id=os.getenv("RUSTFS_ACCESS_KEY"),
+ aws_secret_access_key=os.getenv("RUSTFS_SECRET_KEY"),
+ config=Config(signature_version="s3v4"),
+ region_name="us-east-1",
+)
+
+
+def upload_rustfs(file_obj, filename: str) -> str:
+ _s3.upload_fileobj(file_obj, BUCKET, filename)
+
+ return f"{ENDPOINT}/{BUCKET}/{filename}"