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 + + + + + + + + + \ 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}"