commit 613f426aee25f5d354a22a23435ae41154e12d1a Author: Cxx0822 <1556464090@qq.com> Date: Wed May 27 19:04:09 2026 +0800 feat:初始化工程 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/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e356fa --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..cea8062 --- /dev/null +++ b/agent.py @@ -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 # 系统提示词 +) diff --git a/chief.db b/chief.db new file mode 100644 index 0000000..e491245 Binary files /dev/null and b/chief.db differ diff --git a/chief.db-shm b/chief.db-shm new file mode 100644 index 0000000..0b11a6c Binary files /dev/null and b/chief.db-shm differ diff --git a/chief.db-wal b/chief.db-wal new file mode 100644 index 0000000..4c6b933 Binary files /dev/null and b/chief.db-wal differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..600fc07 --- /dev/null +++ b/main.py @@ -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) diff --git a/model.py b/model.py new file mode 100644 index 0000000..17ad5da --- /dev/null +++ b/model.py @@ -0,0 +1,4 @@ +from pydantic import BaseModel + +class ChatRequest(BaseModel): + message: str \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cb45c85 --- /dev/null +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/router.py b/router.py new file mode 100644 index 0000000..396359c --- /dev/null +++ b/router.py @@ -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} diff --git a/service.py b/service.py new file mode 100644 index 0000000..0fe6574 --- /dev/null +++ b/service.py @@ -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