feat:初始化工程

This commit is contained in:
2026-07-27 20:00:57 +08:00
commit 3ebb249516
16 changed files with 414 additions and 0 deletions

12
.env Normal file
View File

@@ -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

68
.gitignore vendored Normal file
View File

@@ -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/

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/study-agent-service.iml" filepath="$PROJECT_DIR$/.idea/study-agent-service.iml" />
</modules>
</component>
</project>

10
.idea/study-agent-service.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (study-agent-service)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

114
.idea/workspace.xml generated Normal file
View File

@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="c72023a9-a934-49fc-81d0-53a83489c638" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.env" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/study-agent-service.iml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/agent/model.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/agent/prompt.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/agent/study.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/models/agent.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/ocr.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/service.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/storage.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="FastAPI main" />
<option value="FastAPI test_main" />
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 4
}]]></component>
<component name="ProjectId" id="3H4xOetugdBhU5dfrCgc0HWwHSq" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"FastAPI.study-agent-service.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "D:/Cxx/PythonProjects/study-agent-service",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="D:\Cxx\PythonProjects\study-agent-service" />
<recent name="D:\Cxx\PythonProjects\study-agent-service\models" />
<recent name="D:\Cxx\PythonProjects\study-agent-service\agent" />
</key>
</component>
<component name="RunManager">
<configuration name="study-agent-service" type="Python.FastAPI">
<option name="file" value="$PROJECT_DIR$/main.py" />
<module name="study-agent-service" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="launchJavascriptDebuger" value="false" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-1632447f56bf-JavaScript-PY-243.26053.29" />
<option value="bundled-python-sdk-b1dbf8ef85a6-4df51de95216-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-243.26053.29" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="c72023a9-a934-49fc-81d0-53a83489c638" name="Changes" comment="" />
<created>1785141112222</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1785141112222</updated>
<workItem from="1785141113303" duration="9082000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/study_agent_service$study_agent_service.coverage" NAME="study-agent-service Coverage Results" MODIFIED="1785153562933" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
</component>
</project>

13
agent/model.py Normal file
View File

@@ -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')
)

27
agent/prompt.py Normal file
View File

@@ -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"
}
]
}
"""

9
agent/study.py Normal file
View File

@@ -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
)

28
main.py Normal file
View File

@@ -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"
)

5
models/agent.py Normal file
View File

@@ -0,0 +1,5 @@
from pydantic import BaseModel
class QueryRequest(BaseModel):
message: str

53
ocr.py Normal file
View File

@@ -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

10
requirements.txt Normal file
View File

@@ -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

21
service.py Normal file
View File

@@ -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 "信息检索失败,请重新输入问题提问"

24
storage.py Normal file
View File

@@ -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}"