From 20e45d5c8bdcf332bbb81a234fea9a607f1d7da5 Mon Sep 17 00:00:00 2001
From: Cxx0822 <1556464090@qq.com>
Date: Thu, 2 Jul 2026 22:17:33 +0800
Subject: [PATCH] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E6=99=BA=E8=83=BD?=
=?UTF-8?q?=E4=BD=93=E6=9E=B6=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env | 6 +---
.gitignore | 5 +--
.idea/sweet-hut-agent.iml | 3 +-
.idea/workspace.xml | 64 ++++++++++++++++++-------------------
Dockerfile | 11 +++++++
agent.py | 11 -------
agent/bill.py | 11 +++++++
agent/model.py | 22 +++++++++++++
config/db.py | 31 ------------------
main.py | 25 ++++++++-------
model.py | 5 ---
modes/agent.py | 4 +++
prompt/bill.py | 49 ++++++++++++++++++++++++++++
prompt/sql.py | 18 -----------
prompt/system.py | 23 --------------
prompt/table.py | 9 ------
requirements.txt | 23 +++++++-------
router.py | 11 -------
services.py | 46 +++++++++++++++++++++++++++
services/agent_service.py | 44 -------------------------
tools/bill.py | 67 +++++++++++++++++++++++++++++++++++++++
21 files changed, 270 insertions(+), 218 deletions(-)
create mode 100644 Dockerfile
delete mode 100644 agent.py
create mode 100644 agent/bill.py
create mode 100644 agent/model.py
delete mode 100644 config/db.py
delete mode 100644 model.py
create mode 100644 modes/agent.py
create mode 100644 prompt/bill.py
delete mode 100644 prompt/sql.py
delete mode 100644 prompt/system.py
delete mode 100644 prompt/table.py
delete mode 100644 router.py
create mode 100644 services.py
delete mode 100644 services/agent_service.py
create mode 100644 tools/bill.py
diff --git a/.env b/.env
index 2511459..0979efa 100644
--- a/.env
+++ b/.env
@@ -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"
\ No newline at end of file
+TAVILY_API_KEY=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
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index eba1255..7ddac70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,7 +65,4 @@ media/
.DS_Store
logs/
-packages/
-*.db
-*.db-shm
-*.db-wal
\ No newline at end of file
+packages/
\ No newline at end of file
diff --git a/.idea/sweet-hut-agent.iml b/.idea/sweet-hut-agent.iml
index 3d8e3d3..e408242 100644
--- a/.idea/sweet-hut-agent.iml
+++ b/.idea/sweet-hut-agent.iml
@@ -3,8 +3,9 @@
+
-
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index b3b372e..06d7c2f 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -5,23 +5,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -44,26 +39,27 @@
"associatedIndex": 0
}
-
- {
+ "keyToString": {
+ "FastAPI.sweet-hut-agent.executor": "Run",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.typescript.service.memoryLimit.init": "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"
}
-}]]>
+}
@@ -83,6 +79,7 @@
+
@@ -90,8 +87,8 @@
-
-
+
+
@@ -106,6 +103,7 @@
+
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0295eb2
--- /dev/null
+++ b/Dockerfile
@@ -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
diff --git a/agent.py b/agent.py
deleted file mode 100644
index 229299d..0000000
--- a/agent.py
+++ /dev/null
@@ -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,
-)
diff --git a/agent/bill.py b/agent/bill.py
new file mode 100644
index 0000000..ce3c548
--- /dev/null
+++ b/agent/bill.py
@@ -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
+)
diff --git a/agent/model.py b/agent/model.py
new file mode 100644
index 0000000..7cdd87d
--- /dev/null
+++ b/agent/model.py
@@ -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"
+)
\ No newline at end of file
diff --git a/config/db.py b/config/db.py
deleted file mode 100644
index f80b8b9..0000000
--- a/config/db.py
+++ /dev/null
@@ -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
- }
diff --git a/main.py b/main.py
index d9e1fbc..28e5f16 100644
--- a/main.py
+++ b/main.py
@@ -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"
+ )
diff --git a/model.py b/model.py
deleted file mode 100644
index 1ee745d..0000000
--- a/model.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from pydantic import BaseModel
-
-class ChatRequest(BaseModel):
- type: str
- message: str
diff --git a/modes/agent.py b/modes/agent.py
new file mode 100644
index 0000000..882e9c8
--- /dev/null
+++ b/modes/agent.py
@@ -0,0 +1,4 @@
+from pydantic import BaseModel
+
+class QueryRequest(BaseModel):
+ message: str
\ No newline at end of file
diff --git a/prompt/bill.py b/prompt/bill.py
new file mode 100644
index 0000000..150d81b
--- /dev/null
+++ b/prompt/bill.py
@@ -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)的支出账单:”
+- 查询完成后,对账单进行解读(收入 / 支出 / 分类 / 汇总)
+"""
\ No newline at end of file
diff --git a/prompt/sql.py b/prompt/sql.py
deleted file mode 100644
index 56eb12f..0000000
--- a/prompt/sql.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from langchain_core.prompts import PromptTemplate
-
-SQL_PROMPT = PromptTemplate.from_template(
- """
-你是一个 MySQL 专家。
-请根据用户问题生成一条可执行的 MySQL 查询语句。
-只返回 SQL,不要解释,不要加 ```。
-
-数据库结构:
-{schema}
-
-数据库补充:
-{option}
-
-用户问题:
-{question}
-"""
-)
\ No newline at end of file
diff --git a/prompt/system.py b/prompt/system.py
deleted file mode 100644
index 4a12529..0000000
--- a/prompt/system.py
+++ /dev/null
@@ -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. 语气自然,像在和家人沟通。
-
-现在请根据上述内容回答问题。
-"""
-)
diff --git a/prompt/table.py b/prompt/table.py
deleted file mode 100644
index b257c3a..0000000
--- a/prompt/table.py
+++ /dev/null
@@ -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'=收入
-"""
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 5b70968..664cf23 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
\ No newline at end of file
diff --git a/router.py b/router.py
deleted file mode 100644
index 1df5fcb..0000000
--- a/router.py
+++ /dev/null
@@ -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)
diff --git a/services.py b/services.py
new file mode 100644
index 0000000..8fc6ba3
--- /dev/null
+++ b/services.py
@@ -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 "信息检索失败,请重新输入问题提问"
diff --git a/services/agent_service.py b/services/agent_service.py
deleted file mode 100644
index 3a178de..0000000
--- a/services/agent_service.py
+++ /dev/null
@@ -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)}")
diff --git a/tools/bill.py b/tools/bill.py
new file mode 100644
index 0000000..dfa5c07
--- /dev/null
+++ b/tools/bill.py
@@ -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}"