feat:更新智能体架构

This commit is contained in:
2026-07-02 22:17:33 +08:00
parent c66d3a871e
commit 20e45d5c8b
21 changed files with 270 additions and 218 deletions

6
.env
View File

@@ -1,5 +1 @@
SQLITE_DB_PATH="sweet-hut.db"
DB_URL="mysql+pymysql://root:estun%40medical@localhost/sweet_hut?charset=utf8mb4"
MODEL_NAME="qwen3.6-plus"
MODEL_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
MODEL_API_KEY="sk-52bcd98e9c1d45908437c4e8706eefff"
TAVILY_API_KEY=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

3
.gitignore vendored
View File

@@ -66,6 +66,3 @@ media/
logs/
packages/
*.db
*.db-shm
*.db-wal

View File

@@ -3,8 +3,9 @@
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.10 (sweet-hut-agent) (2)" jdkType="Python SDK" />
<orderEntry type="jdk" jdkName="Python 3.12 (sweet-hut-agent) (2)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

64
.idea/workspace.xml generated
View File

@@ -5,23 +5,18 @@
</component>
<component name="ChangeListManager">
<list default="true" id="fca82301-8db7-46f2-bd02-1f76c1dc566b" 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/sweet-hut-agent.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.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/config/db.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/model.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/prompt/sql.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/prompt/system.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/prompt/table.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/router.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/services/agent_service.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.env" beforeDir="false" afterPath="$PROJECT_DIR$/.env" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/agent.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/config/db.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/model.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/prompt/sql.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/prompt/system.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/prompt/table.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/router.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/services/agent_service.py" beforeDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -44,26 +39,27 @@
&quot;associatedIndex&quot;: 0
}</component>
<component name="ProjectId" id="3EcNiHw4Lf7c97fEacOE86P9DSi" />
<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.sweet-hut-agent.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "D:/Cxx/PythonProjects/sweet-hut-agent/prompt",
"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 name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;FastAPI.sweet-hut-agent.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;D:/Cxx/PythonProjects/sweet-hut-agent/prompt&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="D:\Cxx\PythonProjects\sweet-hut-agent\prompt" />
@@ -83,6 +79,7 @@
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="RUN_TOOL" value="" />
<option name="launchJavascriptDebuger" value="false" />
<method v="2" />
</configuration>
@@ -90,8 +87,8 @@
<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" />
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.29346.142" />
<option value="bundled-python-sdk-f2b7a9f6281b-6e1f45a539f7-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.29346.142" />
</set>
</attachedChunks>
</component>
@@ -106,6 +103,7 @@
<workItem from="1780474024998" duration="4337000" />
<workItem from="1780488022005" duration="10000" />
<workItem from="1780971608037" duration="7293000" />
<workItem from="1783001713796" duration="2000" />
</task>
<servers />
</component>

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.12-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", "8000"]
# pip download -r requirements.txt -d ./packages --only-binary=:all: --platform manylinux2014_x86_64 -i https://pypi.tuna.tsinghua.edu.cn/simple

View File

@@ -1,11 +0,0 @@
from langchain.chat_models import init_chat_model
import os
chat_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"),
temperature=1.5,
)

11
agent/bill.py Normal file
View File

@@ -0,0 +1,11 @@
from langchain.agents import create_agent
from agent.model import model
from prompt.bill import bill_prompt
from tools.bill import bill_search
bill_agent = create_agent(
model=model,
tools=[bill_search],
system_prompt=bill_prompt
)

22
agent/model.py Normal file
View File

@@ -0,0 +1,22 @@
from langchain.chat_models import init_chat_model
# model = init_chat_model(
# model="glm-5.1",
# model_provider="openai",
# base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
# api_key="sk-52bcd98e9c1d45908437c4e8706eefff"
# )
# model = init_chat_model(
# model="GLM-5-Turbo",
# model_provider="openai",
# base_url="https://open.bigmodel.cn/api/coding/paas/v4",
# api_key="ea3f96fd542f4bab805a719cf3063cd6.HCRhCV7nqwkDsVKm",
# )
model = init_chat_model(
model="deepseek-v4-flash",
model_provider="openai",
base_url="https://api.deepseek.com",
api_key="sk-0b237d41f6bc44fc9732ea66bd7eade0"
)

View File

@@ -1,31 +0,0 @@
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy import text
load_dotenv()
engine = create_engine(
os.getenv("DB_URL"),
pool_pre_ping=True,
)
conn = engine.connect()
def is_select(sql: str) -> bool:
return sql.lower().lstrip().startswith("select")
def execute_sql(sql: str):
if not is_select(sql):
raise ValueError("只允许 SELECT 查询")
result = conn.execute(text(sql))
rows = result.fetchall()
columns = result.keys()
return {
"columns": list(columns),
"rows": rows
}

25
main.py
View File

@@ -1,22 +1,25 @@
from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from router import router
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import StreamingResponse
# 初始化FastAPI
app = FastAPI(
title="Sweet Hut Agent",
description="智能体",
version="0.1.0"
)
from modes.agent import QueryRequest
from services import query_bill_agent
app = FastAPI(title="Integrate Agent API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
load_dotenv()
app.include_router(router)
@app.post("/query/bill")
async def query_agent(query: QueryRequest):
"""流式对话"""
return StreamingResponse(
query_bill_agent(query),
media_type="text/event-stream"
)

View File

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

4
modes/agent.py Normal file
View File

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

49
prompt/bill.py Normal file
View File

@@ -0,0 +1,49 @@
bill_prompt = """
你是一个个人账单查询助手,负责帮助用户查询账单记录。
# 可调用的工具
- bill_search(start_date, end_date)
- 用于查询指定日期范围内的所有账单
- 日期格式YYYY-MM-DD
- 该接口不区分收入和支出,会一并返回
# 日期推断规则(非常重要)
当用户未明确给出日期时,你必须先按以下规则推断 start_date 和 end_date再调用工具
- 近期 / 最近:今天之前的 30 天
- 本月:当前自然月(如 2026-08-01 ~ 2026-08-31
- 上个月:上一个自然月
- 本季度当前季度Q1: 01-01~03-31依此类推
- 上半年01-01 ~ 06-30
- 下半年07-01 ~ 12-31
- 今年 / 本年:当前自然年
- 去年:上一年自然年
- 无时间描述:默认最近 90 天
# 示例
- 用户:帮我看看最近的账单
→ 推断为最近 30 天 → 调用 bill_search
- 用户:查一下这个月的支出
→ 推断为本月日期范围 → 调用 bill_search后续再用文本过滤支出
- 用户:上半年收入怎么样
→ 推断为 01-01 ~ 06-30 → 调用 bill_search再自行判断 type = income
# 返回字段说明JSON
- id: 账单ID
- bookName: 账本名称
- type: 账单类型income / expense
- category: 账单类别
- location: 账单地点
- payAccount: 支付账户
- amount: 金额
- content: 账单内容
- remark: 备注
- date: 账单日期yyyy-MM-dd
# 行为准则
- 用户未指定日期时,必须先按上述规则推断日期范围
- 查询前应在回复中说明使用的查询条件例如“为您查询本月2026-08-01 ~ 2026-08-31的支出账单
- 查询完成后,对账单进行解读(收入 / 支出 / 分类 / 汇总)
"""

View File

@@ -1,18 +0,0 @@
from langchain_core.prompts import PromptTemplate
SQL_PROMPT = PromptTemplate.from_template(
"""
你是一个 MySQL 专家。
请根据用户问题生成一条可执行的 MySQL 查询语句。
只返回 SQL不要解释不要加 ```。
数据库结构:
{schema}
数据库补充:
{option}
用户问题:
{question}
"""
)

View File

@@ -1,23 +0,0 @@
from langchain_core.prompts import PromptTemplate
BILL_PROMPT = PromptTemplate.from_template(
"""
你是一位家庭账单助手,负责用清晰、专业、易懂的中文回答用户的问题。
用户问题:
{question}
查询到的数据:
{result}
回答要求:
1. 仅基于上方数据进行回答,不要编造内容。
2. 如果数据为空,直接回答:「没有找到相关数据」。
3. 金额统一保留两位小数,并带上“元”。
4. 优先给出结论和总结,再选择性补充关键明细。
5. 不要输出 SQL、字段名、JSON 或技术细节。
6. 语气自然,像在和家人沟通。
现在请根据上述内容回答问题。
"""
)

View File

@@ -1,9 +0,0 @@
BILL_TABLE_SCHEMA = """
【表关系】
bill_record.book_id → bill_book.id
bill_record.category_id → bill_category.id
bill_record.pay_id → bill_pay.id
【字段说明】
bill_record.type / bill_category.type 取值:'expensive'=支出,'income'=收入
"""

View File

@@ -1,12 +1,11 @@
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.13.4
langchain-core~=1.4.0
python-dotenv~=1.2.2
langchain-community~=0.4.2
pymysql~=1.2.0
pymysql~=1.1.2
requests~=2.32.5
fastapi~=0.135.1
langchain-community~=0.4.1
pydantic~=2.12.5
langchain-openai~=1.1.10
uvicorn~=0.23.0
SQLAlchemy~=2.0.48
langchain-core~=1.2.17
langchain~=1.2.10
starlette~=0.52.1

View File

@@ -1,11 +0,0 @@
from fastapi import APIRouter
from model import ChatRequest
from services.agent_service import query
router = APIRouter()
@router.post("/chat")
async def chat_endpoint(request: ChatRequest):
return query(request)

46
services.py Normal file
View File

@@ -0,0 +1,46 @@
from datetime import datetime
from langchain_core.messages import HumanMessage, AIMessageChunk
from agent.bill import bill_agent
from modes.agent import QueryRequest
async def query_bill_agent(query: QueryRequest):
try:
print(query)
today = datetime.today().strftime("%Y-%m-%d")
message_content = (
f"[当前系统日期: {today}]\n\n"
f"{query.message}"
)
message = HumanMessage(content=message_content)
tool_called = False
content_generate = False
# 流式调用Agent
for chunk, metadata in bill_agent.stream(
{"messages": [message]},
stream_mode="messages"
):
# print(chunk)
if isinstance(chunk, AIMessageChunk):
if chunk.content:
if not content_generate:
content_generate = True
yield "\n 📋 **分析结果**\n"
yield chunk.content
for tc in chunk.tool_call_chunks:
if tc.get("args"):
if not tool_called:
tool_called = True
yield "🛠️ **开始查询数据**:查询日期:"
yield tc["args"]
except Exception as e:
print(f"\n[错误]: {str(e)}")
yield "信息检索失败,请重新输入问题提问"

View File

@@ -1,44 +0,0 @@
import os
from langchain.agents import create_agent
from langchain_community.utilities import SQLDatabase
from langchain_core.messages import HumanMessage
from agent import chat_model
from config.db import execute_sql
from model import ChatRequest
from prompt.sql import SQL_PROMPT
from prompt.system import BILL_PROMPT
from prompt.table import BILL_TABLE_SCHEMA
tables = {
"bill": ["bill_book", "bill_category", "bill_pay", "bill_record"]
}
def query(request: ChatRequest):
try:
db = SQLDatabase.from_uri(os.getenv("DB_URL"), include_tables=tables[request.type])
table_info = db.get_table_info()
sql = chat_model.invoke(
SQL_PROMPT.format(schema=table_info, option=BILL_TABLE_SCHEMA, question=request.message)
).content.strip()
print("sql: ", sql)
result = execute_sql(sql)
print("result: ", result)
agent = create_agent(
model=chat_model,
system_prompt=BILL_PROMPT.format(question=request.message, result=result),
)
response = agent.invoke({"message": HumanMessage(content=request.message)})
print("response: ", response)
return response["messages"][-1].content
except Exception as e:
print(f"\n[错误]: {str(e)}")

67
tools/bill.py Normal file
View File

@@ -0,0 +1,67 @@
from langchain_core.tools import tool
import requests
BASE_URL = "https://cxx0822.s.3q.hair/home-api/"
USERNAME = "Cxx0822"
PASSWORD = "19940822Cxx"
def get_token() -> str:
"""
调用登录接口,返回 saToken.tokenValue
"""
params = {
"name": USERNAME,
"password": PASSWORD
}
try:
resp = requests.post(f"{BASE_URL}session", params=params, timeout=10)
resp.raise_for_status()
data = resp.json()
# 按你给的返回结构取值
token_value = data["saToken"]["tokenValue"]
return token_value
except requests.exceptions.RequestException as e:
raise RuntimeError(f"登录失败,无法获取 token: {e}")
except KeyError as e:
raise RuntimeError(f"登录响应结构异常,未找到 tokenValue: {e}")
@tool
def bill_search(start_date: str, end_date: str) -> str:
"""
查询账单列表。
参数说明:
- start_date (必填):
- 开始日期格式YYYY-MM-DD
- 示例2026-06-01
- end_date (必填):
- 结束日期格式YYYY-MM-DD
- 示例2026-07-01
"""
print(f"start_date: {start_date}, end_date: {end_date}")
params = {
"bookName": "",
"type": "",
"startDate": start_date,
"endDate": end_date
}
token = get_token()
headers = {
"satoken": token
}
try:
resp = requests.get(f"{BASE_URL}bill/summary", params=params, headers=headers, timeout=10)
resp.raise_for_status()
return resp.text
except Exception as e:
return f"查询账单失败: {e}"