feat:初始化工程
This commit is contained in:
68
.gitignore
vendored
Normal file
68
.gitignore
vendored
Normal 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/
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM python:3.10-slim
|
||||
WORKDIR /app
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' > /etc/timezone
|
||||
COPY ./packages /app/packages
|
||||
COPY requirements.txt /app/
|
||||
RUN pip install --no-cache-dir --no-index --find-links=/app/packages -r requirements.txt
|
||||
COPY . /app/
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8300"]
|
||||
# pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
45
agent.py
Normal file
45
agent.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from langchain.agents import create_agent
|
||||
from langchain.chat_models import init_chat_model
|
||||
import sqlite3
|
||||
|
||||
from langchain_tavily import TavilySearch
|
||||
from langgraph.checkpoint.sqlite import SqliteSaver
|
||||
|
||||
# 连接sqlite
|
||||
connection = sqlite3.connect("chief.db", check_same_thread=False)
|
||||
# 初始化checkpointer
|
||||
checkpointer = SqliteSaver(connection)
|
||||
# 自动建表
|
||||
checkpointer.setup()
|
||||
|
||||
# web搜索工具,使用tavily作为web搜索工具
|
||||
web_search = TavilySearch(
|
||||
tavily_api_key="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",
|
||||
max_results=5,
|
||||
topic="general"
|
||||
)
|
||||
|
||||
system_prompt = """
|
||||
你是一名私人厨师。收到用户提供的清单后,请按以下流程操作:
|
||||
1.识别和评估食材:根据用户提供的信息,整理出一份“当前可用食材清单”。
|
||||
2.智能食谱检索:优先调用 web_search 工具,以“可用食材清单”为核心关键词,查找可行菜谱。
|
||||
3.多维度评估与排序:从营养价值和制作难度两个维度对检索到的候选食谱进行量化打分,并根据得分排序,制作简单且营养丰富的排名靠前。
|
||||
4.结构化方案输出:把排序后的食谱整理为一份结构清晰的建议报告,要包含食谱信息、得分、推荐理由、食谱的参考图片,帮助用户快速做出决策。
|
||||
|
||||
请严格按照流程,优先调用 web_search 工具搜索食谱,搜索不到的情况下才能自己发挥。
|
||||
"""
|
||||
|
||||
model = init_chat_model(
|
||||
model="qwen3-max-2026-01-23",
|
||||
model_provider="openai",
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
api_key="sk-52bcd98e9c1d45908437c4e8706eefff",
|
||||
temperature=1.5,
|
||||
)
|
||||
|
||||
agent = create_agent(
|
||||
model=model, # 模型
|
||||
tools=[web_search], # 工具
|
||||
checkpointer=checkpointer, # 记忆
|
||||
system_prompt=system_prompt # 系统提示词
|
||||
)
|
||||
BIN
chief.db-shm
Normal file
BIN
chief.db-shm
Normal file
Binary file not shown.
BIN
chief.db-wal
Normal file
BIN
chief.db-wal
Normal file
Binary file not shown.
19
main.py
Normal file
19
main.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from router import router
|
||||
|
||||
# 初始化FastAPI
|
||||
app = FastAPI(
|
||||
title="Personal Chief API",
|
||||
description="私厨",
|
||||
version="0.1.0"
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(router)
|
||||
4
model.py
Normal file
4
model.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
fastapi~=0.136.3
|
||||
uvicorn~=0.48.0
|
||||
langchain~=1.3.2
|
||||
langchain-openai~=1.2.2
|
||||
langchain-tavily~=0.2.18
|
||||
langgraph~=1.2.2
|
||||
langgraph-checkpoint-sqlite~=3.1.0
|
||||
pydantic~=2.12.5
|
||||
langchain-core~=1.4.0
|
||||
32
router.py
Normal file
32
router.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from model import ChatRequest
|
||||
from service import get_messages, clear_messages, search_recipes
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/chat/stream")
|
||||
async def chat_endpoint(request: ChatRequest):
|
||||
"""流式对话"""
|
||||
return StreamingResponse(
|
||||
search_recipes(request.message, str(uuid.uuid4())),
|
||||
media_type="text/event-stream"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/chat/messages")
|
||||
async def get_chat_messages(thread_id: str):
|
||||
"""获取历史消息"""
|
||||
messages = get_messages(thread_id)
|
||||
return {"messages": messages}
|
||||
|
||||
|
||||
@router.delete("/chat/messages")
|
||||
async def clear_chat_messages(thread_id: str):
|
||||
"""清空历史消息"""
|
||||
clear_messages(thread_id)
|
||||
return {"success": True}
|
||||
65
service.py
Normal file
65
service.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from langchain_core.messages import HumanMessage, AIMessageChunk, AIMessage
|
||||
|
||||
from agent import agent, checkpointer
|
||||
|
||||
|
||||
async def search_recipes(prompt: str, thread_id: str):
|
||||
"""调用agent搜索食谱"""
|
||||
print(f"[用户]: {prompt}, thread_id: {thread_id}")
|
||||
try:
|
||||
message = HumanMessage(content=prompt)
|
||||
|
||||
# 流式调用Agent
|
||||
for chunk, metadata in agent.stream(
|
||||
{"messages": [message]},
|
||||
{"configurable": {"thread_id": thread_id}},
|
||||
stream_mode="messages"
|
||||
):
|
||||
if isinstance(chunk, AIMessageChunk) and chunk.content:
|
||||
yield chunk.content
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n[错误]: {str(e)}")
|
||||
yield "信息检索失败,试试看手动输入食物列表?"
|
||||
|
||||
|
||||
# 清空会话
|
||||
def clear_messages(thread_id: str):
|
||||
"""清空会话"""
|
||||
print(f"清空历史消息,thread_id: {thread_id}")
|
||||
checkpointer.delete_thread(thread_id)
|
||||
|
||||
|
||||
# 查询会话历史
|
||||
def get_messages(thread_id: str) -> list[dict[str, str]]:
|
||||
"""获取会话历史"""
|
||||
print(f"获取历史消息,thread_id: {thread_id}")
|
||||
|
||||
# 根据 thread_id 查询 checkpoint
|
||||
checkpoint = checkpointer.get({"configurable": {"thread_id": thread_id}})
|
||||
|
||||
# 如果不存在,返回空列表
|
||||
if not checkpoint:
|
||||
return []
|
||||
|
||||
# 安全获取 messages
|
||||
channel_values = checkpoint.get("channel_values")
|
||||
if not channel_values:
|
||||
return []
|
||||
|
||||
messages = channel_values.get("messages", [])
|
||||
if not messages:
|
||||
return []
|
||||
|
||||
# 转换消息格式
|
||||
result = []
|
||||
for msg in messages:
|
||||
if not msg.content:
|
||||
continue
|
||||
|
||||
if isinstance(msg, HumanMessage):
|
||||
result.append({"role": "user", "content": msg.content})
|
||||
elif isinstance(msg, AIMessage):
|
||||
result.append({"role": "assistant", "content": msg.content})
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user