Compare commits

..

3 Commits

Author SHA1 Message Date
bc44de93bc chef_agent 2026-09-07 13:24:09 +08:00
86ece1926d chef_agent 2026-09-06 19:25:10 +08:00
8efcd72eb2 初始化 2026-09-06 16:54:18 +08:00
20 changed files with 3202 additions and 88 deletions

View File

@@ -18,6 +18,13 @@ DATABASE_POOL_SIZE=10
DATABASE_MAX_OVERFLOW=20 DATABASE_MAX_OVERFLOW=20
DATABASE_ECHO=False DATABASE_ECHO=False
# 数据库Postgres配置
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="123456"
POSTGRES_HOST="127.0.0.1"
POSTGRES_PORT=5432
POSTGRES_DB="postgres"
# CORS配置 # CORS配置
ALLOWED_ORIGINS=["*"] ALLOWED_ORIGINS=["*"]
@@ -32,3 +39,6 @@ UPLOAD_DIR=./uploads
# 缓存配置 # 缓存配置
CACHE_TTL_SECONDS=300 CACHE_TTL_SECONDS=300
# AI配置
DEEPSEEK_API_KEY=api_key

47
api/agent_api.py Normal file
View File

@@ -0,0 +1,47 @@
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from services.agent_server.chef_service import ChefService
# agent相关的路由
agent_router = APIRouter(prefix="/agent", tags=["Agent相关"])
class ChatRequest(BaseModel):
thread_id: str = Field(description="当前会话的id")
message: str = Field(description="当前会话的问题")
class ChatResponse(BaseModel):
thread_id: str
content: str
@agent_router.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
result = ChefService.chat(
thread_id=request.thread_id,
message=request.message,
)
messages = result["messages"]
# 最后一条 AI 消息
last_message = messages[-1]
return ChatResponse(
thread_id=request.thread_id,
content=last_message.content,
)
@agent_router.post("/chat_stream")
def chat(request: ChatRequest):
return StreamingResponse(
ChefService.chat_stream(
thread_id=request.thread_id,
message=request.message,
),
media_type="text/plain; charset=utf-8"
)

View File

@@ -1,7 +1,5 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from typing import Any, Dict from typing import Any, Dict
from core.db import SessionDep
from core.dto.api_response import ApiResponse
from core.task.dlt_scheduler import scheduler from core.task.dlt_scheduler import scheduler
# 彩票相关的路由 # 彩票相关的路由

View File

@@ -0,0 +1,24 @@
import logging
import sys
import structlog
def setup_logging():
logging.basicConfig(
format='%(message)s',
stream=sys.stdout,
level=logging.INFO,
)
structlog.configure(
processors=[
structlog.contextvars.merge_contextvars,
structlog.processors.add_log_level,
structlog.processors.TimeStamper(fmt='iso'),
# structlog.processors.JSONRenderer(ensure_ascii=False), # 生产输出JSON 开发生产2选一
structlog.dev.ConsoleRenderer() # 开发环境彩色控制台
],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)

View File

@@ -94,6 +94,30 @@ class Settings(BaseSettings):
description="是否打印SQL语句" description="是否打印SQL语句"
) )
# ---------- 数据库Postgres配置 ----------
POSTGRES_USER: str = Field(
default='postgres',
description="postgres用户名"
)
POSTGRES_PASSWORD: str = Field(
default='123456',
description="postgres密码"
)
POSTGRES_HOST: str = Field(
default='127.0.0.1',
description="postgres主机地址"
)
POSTGRES_PORT: int = Field(
default=5432,
ge=1,
le=65535,
description="postgres端口"
)
POSTGRES_DB: str = Field(
default='postgres',
description="postgres数据库名"
)
# ---------- CORS配置 ---------- # ---------- CORS配置 ----------
ALLOWED_ORIGINS: List[str] = Field( ALLOWED_ORIGINS: List[str] = Field(
default=["*"], default=["*"],
@@ -143,6 +167,17 @@ class Settings(BaseSettings):
description="缓存过期时间(秒)" description="缓存过期时间(秒)"
) )
# ---------- AI配置 ----------
DEEPSEEK_API_KEY: str = Field(
default="sk-d3e11a4229744fd29a8468e7df072a4b",
description="deepseek模型api_key"
)
TAVILY_API_KEY: str = Field(
default="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",
description="tavily搜索接口api_key"
)
# ---------- 计算属性 ---------- # ---------- 计算属性 ----------
@property @property
def mysql_url(self) -> str: def mysql_url(self) -> str:
@@ -150,6 +185,12 @@ class Settings(BaseSettings):
# 连接url格式mysql+pymysql://user:password@host:port/dbname # 连接url格式mysql+pymysql://user:password@host:port/dbname
return f"mysql+pymysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DB}?charset=utf8mb4" return f"mysql+pymysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DB}?charset=utf8mb4"
@property
def postgres_url(self) -> str:
"""postgres连接URL"""
# 连接url格式postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}?sslmode=disable"
@property @property
def is_production(self) -> bool: def is_production(self) -> bool:
"""是否为生产环境""" """是否为生产环境"""

View File

@@ -1,30 +1,16 @@
import logging
import sys
from pathlib import Path
from datetime import datetime from datetime import datetime
from typing import List from typing import List
import structlog
from apscheduler.job import Job from apscheduler.job import Job
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.schedulers.base import STATE_PAUSED, STATE_STOPPED, STATE_RUNNING from apscheduler.schedulers.base import STATE_PAUSED, STATE_STOPPED, STATE_RUNNING
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR from apscheduler.events import EVENT_JOB_EXECUTED, EVENT_JOB_ERROR
# 添加项目根目录到路径
sys.path.append(str(Path(__file__).parent.parent.parent))
from core.task.dlt_spider import DLTSpider from core.task.dlt_spider import DLTSpider
# 配置日志 logger = structlog.getLogger(__name__)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('lottery_scheduler.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
def test_fun(): def test_fun():
logger.info(f"当前时间---{datetime.now()}") logger.info(f"当前时间---{datetime.now()}")

View File

@@ -1,17 +1,16 @@
import logging
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from typing import Any, List, Dict, Union, Optional, NamedTuple from typing import Any, List, Dict, Union, Optional, NamedTuple
import time import time
import requests import requests
from pydantic import BaseModel, field_validator import structlog
from sqlalchemy import desc from sqlalchemy import desc
from core.db import get_db_session from core.db import get_db_session
from models import LotteryDLT from models import LotteryDLT
logger = logging.getLogger(__name__) logger = structlog.getLogger(__name__)
def parse_draw_numbers(result_str: str) -> List[str]: def parse_draw_numbers(result_str: str) -> List[str]:

0
exceptions/__init__.py Normal file
View File

3
exceptions/errors.py Normal file
View File

@@ -0,0 +1,3 @@
class AgentNotReadyError(RuntimeError):
"""agent未初始化错误"""
pass

24
exceptions/handlers.py Normal file
View File

@@ -0,0 +1,24 @@
from fastapi import Request, FastAPI
from fastapi.responses import JSONResponse
from exceptions.errors import AgentNotReadyError
async def agent_not_ready_handler(request: Request, exc: Exception):
"""agent未就绪的错误"""
return JSONResponse(
status_code=503,
content={
"code": 503,
"message": str(exc)
}
)
# 集中处理 保持main简洁
def register_exception_handlers(app: FastAPI):
"""集中处理注册异常"""
app.add_exception_handler(
AgentNotReadyError,
agent_not_ready_handler,
)

0
lc/__init__.py Normal file
View File

255
lc/chef_agent.py Normal file
View File

@@ -0,0 +1,255 @@
from __future__ import annotations
from typing import Any, cast
import structlog
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langchain.agents.middleware.summarization import ContextMessages
from langchain.chat_models import init_chat_model
from langchain_core.language_models import BaseChatModel
from langchain_tavily import TavilySearch
from langgraph.checkpoint.postgres import PostgresSaver
from psycopg import Connection
from psycopg_pool import ConnectionPool
from core.config.settings import settings
from exceptions.errors import AgentNotReadyError
logger = structlog.getLogger(__name__)
class ChefAgent:
"""厨师 Agent。
负责:
- LLM 初始化
- Tool 初始化
- CheckPointer 初始化
- Middleware 初始化
- Agent 初始化
- 生命周期管理
"""
def __init__(self) -> None:
self._pool = None
self._checkpointer: PostgresSaver | None = None
self._agent = None
def initialize(self) -> ChefAgent:
"""初始化 Agent。
Returns:
当前 Agent 实例,方便链式调用。
"""
logger.info("开始初始化chef_agent...")
if self._agent is not None:
return self
# 1. 初始化 LLM
llm = self._create_llm()
# 2. 初始化工具
tools = self._create_tools()
# 3. 初始化 Middleware
middleware = self._create_middleware(llm)
# 4. 初始化 CheckPointer
self._checkpointer = self._create_checkpointer()
# 5. 初始化 Agent
logger.info("chef_agent的agent开始初始化...")
self._agent = create_agent(
model=llm,
tools=tools,
middleware=middleware,
checkpointer=self._checkpointer,
system_prompt=self._system_prompt(),
)
logger.info("chef_agent的agent初始化完成")
logger.info("chef_agent初始化完成")
return self
@staticmethod
def _create_llm() -> BaseChatModel:
"""创建 LLM。"""
logger.info("chef_agent初始化llm...")
llm = init_chat_model(
model="deepseek-v4-flash",
model_provider="deepseek",
api_key=settings.DEEPSEEK_API_KEY
)
logger.info("chef_agent的llm初始化完成")
return cast(BaseChatModel, llm)
@staticmethod
def _create_tools() -> list[Any]:
"""创建 Agent Tools。"""
logger.info("chef_agent工具开始初始化...")
search_tool = TavilySearch(
tavily_api_key=settings.TAVILY_API_KEY,
max_results=5,
topic="general",
)
logger.info("chef_agent工具初始化完成")
return [search_tool]
@staticmethod
def _create_middleware(
llm: BaseChatModel,
) -> list[Any]:
"""创建 Agent Middleware。"""
logger.info("chef_agent中间件开始初始化...")
summarization = SummarizationMiddleware(
model=llm,
trigger=cast(ContextMessages, ("messages", 10)),
keep=cast(ContextMessages, ("messages", 5)),
)
logger.info("chef_agent中间件初始化完成")
return [summarization]
def _create_checkpointer(self) -> PostgresSaver:
"""创建 PostgreSQL CheckPointer。"""
logger.info("chef_agent的CheckPointer开始初始化")
logger.info("创建PostgreSQL连接池")
self._pool = ConnectionPool[Connection[dict[str, Any]]]( # 指定类型[Connection[dict[str, Any]]]防止编译器提示错误
conninfo=settings.postgres_url,
max_size=10,
kwargs={
"autocommit": True,
}
)
logger.info("创建checkpointer")
checkpointer = PostgresSaver(self._pool)
logger.info("初始化checkpoint数据表")
# 创建 LangGraph checkpoint 所需的数据表
checkpointer.setup()
logger.info("chef_agent的CheckPointer初始化完成")
return checkpointer
@staticmethod
def _system_prompt() -> str:
"""Agent System Prompt。"""
return """
你是一名专业厨师。
你的任务是根据用户提供的食材照片或食材清单,为用户推荐合适的菜谱。
请严格按照以下流程执行:
## 1. 识别和评估食材
如果用户提供的是食材照片:
- 识别照片中可见的食材
- 根据外观判断食材的新鲜程度
- 估算大致可用量
- 排除明显不可食用或状态异常的食材
整理成「可用食材清单」。
如果用户直接提供食材清单,则直接使用用户提供的信息。
## 2. 搜索菜谱
优先使用搜索工具。
以「可用食材清单」作为核心关键词,搜索适合这些食材的菜谱。
优先考虑:
- 食材匹配度高
- 操作简单
- 营养均衡
- 家庭烹饪可执行
除非搜索不到合适结果,否则不要直接凭经验编造菜谱。
## 3. 评估和排序
对搜索到的候选菜谱进行综合评价。
从以下维度进行评分:
- 食材匹配度
- 营养价值
- 烹饪难度
- 烹饪时间
综合评分后进行排序。
优先推荐:
「简单 + 食材匹配度高 + 营养丰富」
的菜谱。
## 4. 输出结果
最终输出结构化的菜谱推荐报告。
每个推荐至少包含:
- 菜谱名称
- 所需食材
- 核心烹饪步骤
- 烹饪时间
- 难度
- 综合评分
- 推荐理由
- 参考来源
如果搜索结果中存在可靠的菜谱图片,则提供图片参考。
如果搜索不到合适菜谱,再根据已有知识进行合理推荐,并明确说明这是基于模型知识给出的建议。
""".strip()
@property
def agent(self):
"""获取 Agent。"""
if self._agent is None:
logger.info("ChefAgent 尚未初始化,请先调用 initialize()")
raise AgentNotReadyError(
"ChefAgent 尚未初始化,请先调用 initialize()"
)
return self._agent
def close(self) -> None:
"""释放资源。"""
if self._pool is not None:
self._pool.close()
self._pool = None
logger.info("PostgreSQL已关闭")
self._checkpointer = None
self._agent = None
# 使用with上下文管理方式来使用的话需要这2个方法
# def __enter__(self) -> ChefAgent:
# return self.initialize()
#
# def __exit__(self, exc_type, exc_value, traceback) -> None:
# self.close()
chef_agent = ChefAgent()

194
lc/deepseek.ipynb Normal file
View File

@@ -0,0 +1,194 @@
{
"cells": [
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-03T09:14:58.535236Z",
"start_time": "2026-09-03T09:14:57.335343800Z"
}
},
"cell_type": "code",
"source": [
"from openai import OpenAI\n",
"from openai.types.chat import (\n",
" ChatCompletionSystemMessageParam,\n",
" ChatCompletionUserMessageParam,\n",
" ChatCompletionAssistantMessageParam\n",
")"
],
"id": "255e9b078be17a5f",
"outputs": [],
"execution_count": 1
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-03T09:19:09.642201600Z",
"start_time": "2026-09-03T09:19:09.628764300Z"
}
},
"cell_type": "code",
"source": [
"client = OpenAI(\n",
" api_key='sk-4bdcdad5d4cd4856bc0c308c3f74eb22',\n",
" base_url='https://api.deepseek.com'\n",
")"
],
"id": "6f8aeb64ad6aca1f",
"outputs": [],
"execution_count": 6
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-03T09:17:52.034706300Z",
"start_time": "2026-09-03T09:17:52.023338600Z"
}
},
"cell_type": "code",
"source": [
"messages: list[ChatCompletionSystemMessageParam | ChatCompletionUserMessageParam | ChatCompletionAssistantMessageParam] = [\n",
" ChatCompletionSystemMessageParam(role=\"system\", content=\"你是一个ai助手所有回答使用中文\")\n",
"]"
],
"id": "4530b047d03a8ab3",
"outputs": [],
"execution_count": 3
},
{
"metadata": {},
"cell_type": "code",
"outputs": [],
"execution_count": null,
"source": [
"# 第一轮对话\n",
"print(\"===========\")\n",
"messages.append(ChatCompletionUserMessageParam(role=\"user\", content=\"你好,我是大哥\"))\n",
"\n",
"response = client.chat.completions.create(\n",
" model='deepseek-v4-flash',\n",
" messages=messages,\n",
" stream=False\n",
")"
],
"id": "a42f7a8dbdfa3b8c"
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-03T08:51:19.785432800Z",
"start_time": "2026-09-03T08:51:19.759474700Z"
}
},
"cell_type": "code",
"source": [
"# print(response.model_dump_json())\n",
"print(response.choices[0].message.content)"
],
"id": "fac34b2742f048e0",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"大哥你好我是你的AI助手随时听候差遣。今天有什么需要帮忙的吗\n"
]
}
],
"execution_count": 12
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-03T08:51:41.715208300Z",
"start_time": "2026-09-03T08:51:36.483133100Z"
}
},
"cell_type": "code",
"source": [
"# 第二轮对话\n",
"messages.append(ChatCompletionAssistantMessageParam(role=\"assistant\", content=response.choices[0].message.content))\n",
"messages.append(ChatCompletionUserMessageParam(role=\"user\", content=\"你还记得我吗\"))\n",
"response = client.chat.completions.create(\n",
" model='deepseek-v4-flash',\n",
" messages=messages,\n",
")\n",
"print(response.choices[0].message.content)"
],
"id": "3c9fa1f10b75c256",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"大哥,这个问题您刚刚才问过呢,我当然记得您!您就是“大哥”嘛,我怎么会忘?\n",
"\n",
"虽然我的记忆不能跨对话永久保存,但只要咱们在这个对话里,您说的每句话、问的每个问题,我都记得一清二楚——包括您刚才已经问过一次“还记得我吗”,我当时也解释过啦。\n",
"\n",
"您现在又问一遍,是在考验我,还是想看看我是不是“脸盲”呀?哈哈,放心吧!在这段对话期间,您永远是我的大哥,随叫随到,有事您吩咐!😄\n"
]
}
],
"execution_count": 14
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-03T09:19:38.275536400Z",
"start_time": "2026-09-03T09:19:31.090446900Z"
}
},
"cell_type": "code",
"source": [
"messages.append(ChatCompletionUserMessageParam(role=\"user\", content=\"你好,明天合肥天气怎么样\"))\n",
"\n",
"response = client.chat.completions.create(\n",
" model='deepseek-v4-flash',\n",
" messages=messages,\n",
" stream=False\n",
")\n",
"print(response.choices[0].message.content)"
],
"id": "ea34cc88d92b2832",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"你好!很抱歉,由于我无法实时联网获取最新的气象数据,所以不能直接为你提供明天合肥的准确天气预报。\n",
"\n",
"不过,你可以通过以下几种方式快速查到最准确的信息:\n",
"\n",
"1. 打开手机自带的“天气”应用,添加并定位到“合肥”。\n",
"2. 在搜索引擎(如百度)中直接搜索“**合肥明天天气**”,或其他(例如“墨迹天气”)\n",
"3. 访问“中国天气网”www.weather.com.cn输入“合肥”即可查询。\n",
"\n",
"另外,由于我无法预知你目前的实际日期,建议你在查询时留意一下,如果是明天出发,记得顺带看一眼**后天**的预报,以便更好地安排行程。祝生活愉快!\n"
]
}
],
"execution_count": 7
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

167
lc/lc.ipynb Normal file
View File

@@ -0,0 +1,167 @@
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-09-05T01:34:44.550869800Z",
"start_time": "2026-09-05T01:34:44.535351500Z"
}
},
"source": [
"from typing import cast, Literal\n",
"\n",
"from langchain.agents.middleware.summarization import ContextMessages\n",
"from langchain.chat_models import init_chat_model\n",
"from langchain.agents import create_agent\n",
"from langchain_core.runnables import RunnableConfig\n",
"from langgraph.checkpoint.memory import InMemorySaver\n",
"from langchain.agents.middleware import SummarizationMiddleware\n",
"\n",
"from core.config.settings import settings\n",
"from langchain.tools import tool\n",
"from langchain.messages import HumanMessage\n",
"from langchain_core.language_models import BaseChatModel"
],
"outputs": [],
"execution_count": 15
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-05T01:37:24.572533800Z",
"start_time": "2026-09-05T01:37:24.555523100Z"
}
},
"cell_type": "code",
"source": [
"# 初始化模型\n",
"my_model = init_chat_model(\n",
" model=\"deepseek-v4-flash\",\n",
" api_key=settings.DEEPSEEK_API_KEY,\n",
")\n",
"\n",
"# 初始化checkpointer 记忆管理的存储方式\n",
"checkpointer = InMemorySaver()\n",
"\n",
"# 初始化记忆策略中间件\n",
"middleware = SummarizationMiddleware(\n",
" model=cast(BaseChatModel, my_model), # 消息摘要的记忆管理策略的模型\n",
" trigger=cast(ContextMessages, (\"messages\", 3)), # 触发策略的条件\n",
" keep=cast(ContextMessages, (\"messages\", 1)) # 触发策略后保留的消息条数\n",
")"
],
"id": "b905be968f5c7761",
"outputs": [],
"execution_count": 22
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-05T01:37:26.422888800Z",
"start_time": "2026-09-05T01:37:26.408643600Z"
}
},
"cell_type": "code",
"source": [
"# 初始化agent\n",
"agent = create_agent(\n",
" model=cast(BaseChatModel, my_model),\n",
" checkpointer=InMemorySaver(), # 短期记忆 通过thread_id进行记忆分组\n",
" middleware=[middleware]\n",
")"
],
"id": "cb5ae2fa4aeaf130",
"outputs": [],
"execution_count": 23
},
{
"metadata": {
"ExecuteTime": {
"end_time": "2026-09-05T01:37:59.930524700Z",
"start_time": "2026-09-05T01:37:27.968100300Z"
}
},
"cell_type": "code",
"source": [
"config: RunnableConfig = {\"configurable\": {\"thread_id\": \"thread_1\"}}\n",
"agent.invoke({\"messages\": [HumanMessage(\"你好,我是胖哥\")]}, config)\n",
"agent.invoke({\"messages\": [HumanMessage(\"我喜欢吃美食\")]}, config)\n",
"agent.invoke({\"messages\": [HumanMessage(\"我喜欢运动\")]}, config)\n",
"\n",
"result = agent.invoke({\"messages\": [HumanMessage(\"你还记得我吗\")]}, config)\n",
"# print(result)\n",
"for message in result['messages']:\n",
" message.pretty_print()"
],
"id": "1ed1327c973d75cc",
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"================================\u001B[1m Human Message \u001B[0m=================================\n",
"\n",
"Here is a summary of the conversation to date:\n",
"\n",
"## SESSION INTENT\n",
"\n",
"开放式、友好的中文闲聊会话,无具体交付物。用户自称“胖哥”,目前已透露两大爱好:美食与运动。整体目标是延续话题、维持轻松氛围,进一步了解他的偏好,从而围绕美食/运动展开聊天或提供推荐。\n",
"\n",
"## SUMMARY\n",
"\n",
"- 用户为“胖哥”,须以中文称呼;全会话均使用中文。\n",
"- 话题线索已在两条线上推进:\n",
" 1. **美食**:胖哥说“我喜欢吃美食”。助手此前已问他偏好哪些菜系(川菜/粤菜/湘菜)以及喜欢哪种类型(街边小吃、家常菜还是精致餐饮),并邀请他分享最近特别喜欢的一道菜。胖哥尚未回答此组问题。\n",
" 2. **运动(新增)**:胖哥随后说“我喜欢运动”。助手将美食与运动联系起来称赞(爱吃又会动、搭配健康),并追问:喜欢哪种运动——健身房撸铁、户外跑步骑行,还是打篮球羽毛球等对抗性项目;同时主动提出可以推荐运动后补充能量的美食搭配。\n",
"- 尚无任何结论、决定或策略形成;没有选项被否决。\n",
"- 需要注意的是,胖哥对“美食偏好”相关问题尚未作答,该线索仍处于待回应状态。\n",
"\n",
"## ARTIFACTS\n",
"\n",
"None.\n",
"\n",
"## NEXT STEPS\n",
"\n",
"- 等待胖哥回复:他喜欢哪种运动(健身房/户外/球类对抗等)。\n",
"- 得到答复后顺势深入聊天:讨论该运动,可结合他未答复的那组问题,推荐适合运动后的营养美食搭配,或顺带再把菜系/餐饮类型偏好问出来,延长话题。\n",
"- 不要重复寒暄,也不要重复之前已问过的两组问题(菜系类型与偏好风格、运动类型选择)。\n",
"- 保持中文、热情随意的口吻继续对话。\n",
"================================\u001B[1m Human Message \u001B[0m=================================\n",
"\n",
"你还记得我吗\n",
"==================================\u001B[1m Ai Message \u001B[0m==================================\n",
"\n",
"胖哥,这话说的——当然记得你呀!爱美食又爱运动,能吃会练,这反差感可太让人印象深刻了。咱俩这不正聊到运动嘛,我还等着听你细说呢。 \n",
"\n",
"不过不急,你先说说最近一次痛快出汗是啥时候?是去健身房撸铁,还是户外疯跑了一圈?反正不管哪种,你要是一会儿饿了,我脑子里的运动后美食搭配可已经在排队了😄\n"
]
}
],
"execution_count": 24
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

41
main.py
View File

@@ -1,23 +1,24 @@
import logging import structlog
import models import models
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel from sqlmodel import SQLModel
from api.agent_api import agent_router
from core.config.logging_cof import setup_logging
from core.db import engine from core.db import engine
from core.config.settings import settings from core.config.settings import settings
from api.scheduler_api import scheduler_router from api.scheduler_api import scheduler_router
from api.lottery_api import lottery_router from api.lottery_api import lottery_router
from core.task.dlt_scheduler import scheduler from core.task.dlt_scheduler import scheduler
from exceptions.handlers import register_exception_handlers
from lc.chef_agent import chef_agent
# 置日志 # 全局设置日志配置
logging.basicConfig( setup_logging()
level=logging.INFO, logger = structlog.getLogger(__name__)
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# lifespan生命周期 # lifespan生命周期
@asynccontextmanager @asynccontextmanager
@@ -27,18 +28,34 @@ async def lifespan(app: FastAPI):
# 同步建表 # 同步建表
SQLModel.metadata.create_all(engine) SQLModel.metadata.create_all(engine)
logger.info("服务启动,启动定时调度器")
# 启动定时调度器 # 启动定时调度器
scheduler.start() scheduler.start()
yield # 此处交出控制权,服务开始运行 logger.info("服务启动创建携程初始化chef_agent任务")
# ========== 关闭时执行 ========== # 异步初始化chef_agent
logger.info("服务关闭,释放资源") chef_agent.initialize()
scheduler.shutdown()
logger.info("服务启动完成")
try:
yield # 此处交出控制权,服务开始运行
finally:
# ========== 关闭时执行 ==========
logger.info("服务关闭,释放资源")
chef_agent.close()
scheduler.shutdown()
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
# 注册全局异常处理
register_exception_handlers(app)
# 注册路由
app.include_router(scheduler_router) app.include_router(scheduler_router)
app.include_router(lottery_router) app.include_router(lottery_router)
app.include_router(agent_router)
# 注册中间件
app.add_middleware( app.add_middleware(
CORSMiddleware, # type: ignore[arg-type] CORSMiddleware, # type: ignore[arg-type]
allow_origins=settings.ALLOWED_ORIGINS, allow_origins=settings.ALLOWED_ORIGINS,

View File

@@ -1,11 +1,23 @@
[project] [project]
name = "lotteryserver" name = "lotteryserver"
version = "0.1.0" version = "0.1.0"
requires-python = ">=3.14" requires-python = ">=3.13"
dependencies = [ dependencies = [
"apscheduler==3.11.3", "apscheduler==3.11.3",
"fastapi>=0.141.1", "fastapi>=0.141.1",
"ipykernel>=7.3.0", "ipykernel>=7.3.0",
"langchain>=1.4.0",
"langchain-core==1.6.2",
"langchain-deepseek==1.1.0",
"langchain-tavily==0.2.18",
"langgraph>=1.2.11",
"langgraph-checkpoint-postgres>=3.1.2",
"langgraph-checkpoint-sqlite==3.1.1",
"notebook>=7.6.2",
"openai==3.8.0",
"psycopg>=3.3.5",
"psycopg-binary>=3.3.5",
"psycopg-pool>=3.3.1",
"pydantic==2.13.5", "pydantic==2.13.5",
"pydantic-settings==2.15.0", "pydantic-settings==2.15.0",
"pymysql==1.2.0", "pymysql==1.2.0",
@@ -13,5 +25,6 @@ dependencies = [
"sqlalchemy>=2.0.52", "sqlalchemy>=2.0.52",
"sqlmodel==0.0.42", "sqlmodel==0.0.42",
"starlette>=1.6.0", "starlette>=1.6.0",
"structlog>=26.1.0",
"uvicorn==0.52.4", "uvicorn==0.52.4",
] ]

View File

View File

@@ -0,0 +1,29 @@
from typing import Iterator
from langchain_core.messages import HumanMessage
from langchain_core.runnables import RunnableConfig
from lc.chef_agent import chef_agent
class ChefService:
@staticmethod
def chat(
thread_id: str,
message: str,
):
config: RunnableConfig = {"configurable": {"thread_id": thread_id}}
result = chef_agent.agent.invoke({"messages": [HumanMessage(content=message)]}, config)
return result
@staticmethod
def chat_stream(thread_id: str, message: str) -> Iterator[str]:
config: RunnableConfig = {"configurable": {"thread_id": thread_id}}
result = chef_agent.agent.stream({"messages": [("user", message)]}, config=config, stream_mode="messages")
for message_chunk, metadata in result:
content = message_chunk.content
if content:
yield content

20
test.py Normal file
View File

@@ -0,0 +1,20 @@
a: int | None = None
def f(v: int) -> int:
pass
def f2(v: int) -> int:
a = 5
f(a)
class Test:
def __init__(self):
self.a: int | None = None
def f(self, v: int) -> int:
pass
def f2(self):
self.a = 5
self.f(self.a)

2395
uv.lock generated

File diff suppressed because it is too large Load Diff