Compare commits

...

5 Commits

Author SHA1 Message Date
93ef589826 chef_agent 2026-09-11 10:08:58 +08:00
3405d1b355 chef_agent 2026-09-09 09:15:12 +08:00
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
25 changed files with 3723 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

262
lc/chef_agent.py Normal file
View File

@@ -0,0 +1,262 @@
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_deepseek import ChatDeepSeek
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,
# extra_body={"thingking":{"type": "disabled"}}
# )
llm = ChatDeepSeek(
model="deepseek-v4-flash",
api_key=settings.DEEPSEEK_API_KEY,
extra_body={"thingking": {"type": "disabled"}}
)
logger.info("chef_agent的llm初始化完成")
return 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
}

0
lg/__init__.py Normal file
View File

194
lg/approve.ipynb Normal file

File diff suppressed because one or more lines are too long

131
lg/fan_in.ipynb Normal file

File diff suppressed because one or more lines are too long

111
lg/node_error.ipynb Normal file
View File

@@ -0,0 +1,111 @@
{
"cells": [
{
"cell_type": "code",
"id": "initial_id",
"metadata": {
"collapsed": true,
"ExecuteTime": {
"end_time": "2026-09-08T07:56:57.833862800Z",
"start_time": "2026-09-08T07:56:51.946284300Z"
}
},
"source": [
"import asyncio\n",
"import logging\n",
"\n",
"from IPython.display import display\n",
"from langchain_core.runnables import RunnableConfig\n",
"from langgraph.constants import START, END\n",
"from langgraph.graph import StateGraph\n",
"from langgraph.types import RetryPolicy\n",
"from pydantic import BaseModel\n",
"from requests import HTTPError\n",
"\n",
"logger = logging.getLogger(__name__)\n",
"\n",
"\n",
"# 全局状态\n",
"class EmptyState(BaseModel):\n",
" pass\n",
"\n",
"async def node_a(state: EmptyState, config: RunnableConfig) -> EmptyState:\n",
" print(\"11111111111\")\n",
" await asyncio.sleep(5)\n",
" raise HTTPError(\"node_a\")\n",
"\n",
"\n",
"builder = StateGraph(state_schema=EmptyState)\n",
"builder.add_node(\"node_a\", node_a, retry_policy=RetryPolicy(max_attempts=3), timeout=1)\n",
"\n",
"builder.add_edge(START, \"node_a\")\n",
"builder.add_edge(\"node_a\", END)\n",
"\n",
"workflow = builder.compile()\n",
"try:\n",
" result = await workflow.ainvoke({})\n",
" # print(result)\n",
"except HTTPError as e:\n",
" pass\n",
"\n",
"\n",
"display(workflow)"
],
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"11111111111\n",
"11111111111\n",
"11111111111\n"
]
},
{
"ename": "NodeTimeoutError",
"evalue": "Node 'node_a' exceeded its run timeout of 1.000s (elapsed: 1.007s).",
"output_type": "error",
"traceback": [
"\u001B[31m---------------------------------------------------------------------------\u001B[39m",
"\u001B[31mTimeoutError\u001B[39m Traceback (most recent call last)",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_retry.py:489\u001B[39m, in \u001B[36m_arun_with_timeout\u001B[39m\u001B[34m(task, config, timeout, attempt_ctx, stream)\u001B[39m\n\u001B[32m 488\u001B[39m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[32m--> \u001B[39m\u001B[32m489\u001B[39m \u001B[38;5;28;01mawait\u001B[39;00m watchdog\n\u001B[32m 490\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m asyncio.TimeoutError \u001B[38;5;28;01mas\u001B[39;00m exc:\n",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_retry.py:419\u001B[39m, in \u001B[36m_run_timeout_watchdog\u001B[39m\u001B[34m(run_timeout_s)\u001B[39m\n\u001B[32m 418\u001B[39m \u001B[38;5;28;01mawait\u001B[39;00m asyncio.sleep(run_timeout_s)\n\u001B[32m--> \u001B[39m\u001B[32m419\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m asyncio.TimeoutError\n",
"\u001B[31mTimeoutError\u001B[39m: ",
"\nThe above exception was the direct cause of the following exception:\n",
"\u001B[31mNodeTimeoutError\u001B[39m Traceback (most recent call last)",
"\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[25]\u001B[39m\u001B[32m, line 36\u001B[39m\n\u001B[32m 32\u001B[39m workflow = builder.compile()\n\u001B[32m 33\u001B[39m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[32m 34\u001B[39m result = \u001B[38;5;28;01mawait\u001B[39;00m workflow.ainvoke({})\n\u001B[32m 35\u001B[39m \u001B[38;5;66;03m# print(result)\u001B[39;00m\n\u001B[32m---> \u001B[39m\u001B[32m36\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m HTTPError \u001B[38;5;28;01mas\u001B[39;00m e:\n\u001B[32m 37\u001B[39m \u001B[38;5;28;01mpass\u001B[39;00m\n\u001B[32m 38\u001B[39m \n\u001B[32m 39\u001B[39m \n",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\main.py:4090\u001B[39m, in \u001B[36mPregel.ainvoke\u001B[39m\u001B[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, control, version, **kwargs)\u001B[39m\n\u001B[32m 4087\u001B[39m chunks.append(chunk)\n\u001B[32m 4088\u001B[39m \u001B[38;5;28;01melse\u001B[39;00m:\n\u001B[32m 4089\u001B[39m \u001B[38;5;66;03m# v1: collect interrupts from updates stream\u001B[39;00m\n\u001B[32m-> \u001B[39m\u001B[32m4090\u001B[39m \u001B[38;5;28;01masync\u001B[39;00m \u001B[38;5;28;01mfor\u001B[39;00m chunk \u001B[38;5;129;01min\u001B[39;00m \u001B[38;5;28mself\u001B[39m.astream(\n\u001B[32m 4091\u001B[39m \u001B[38;5;28minput\u001B[39m,\n\u001B[32m 4092\u001B[39m config,\n\u001B[32m 4093\u001B[39m context=context,\n\u001B[32m 4094\u001B[39m stream_mode=(\n\u001B[32m 4095\u001B[39m [\u001B[33m\"\u001B[39m\u001B[33mupdates\u001B[39m\u001B[33m\"\u001B[39m, \u001B[33m\"\u001B[39m\u001B[33mvalues\u001B[39m\u001B[33m\"\u001B[39m] \u001B[38;5;28;01mif\u001B[39;00m stream_mode == \u001B[33m\"\u001B[39m\u001B[33mvalues\u001B[39m\u001B[33m\"\u001B[39m \u001B[38;5;28;01melse\u001B[39;00m stream_mode\n\u001B[32m 4096\u001B[39m ),\n\u001B[32m 4097\u001B[39m print_mode=print_mode,\n\u001B[32m 4098\u001B[39m output_keys=output_keys,\n\u001B[32m 4099\u001B[39m interrupt_before=interrupt_before,\n\u001B[32m 4100\u001B[39m interrupt_after=interrupt_after,\n\u001B[32m 4101\u001B[39m durability=durability,\n\u001B[32m 4102\u001B[39m control=control,\n\u001B[32m 4103\u001B[39m **kwargs,\n\u001B[32m 4104\u001B[39m ):\n\u001B[32m 4105\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m stream_mode == \u001B[33m\"\u001B[39m\u001B[33mvalues\u001B[39m\u001B[33m\"\u001B[39m:\n\u001B[32m 4106\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mlen\u001B[39m(chunk) == \u001B[32m2\u001B[39m:\n",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\main.py:3440\u001B[39m, in \u001B[36mPregel.astream\u001B[39m\u001B[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, control, subgraphs, debug, version, **kwargs)\u001B[39m\n\u001B[32m 3438\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m task \u001B[38;5;129;01min\u001B[39;00m \u001B[38;5;28;01mawait\u001B[39;00m loop.amatch_cached_writes():\n\u001B[32m 3439\u001B[39m loop.output_writes(task.id, task.writes, cached=\u001B[38;5;28;01mTrue\u001B[39;00m)\n\u001B[32m-> \u001B[39m\u001B[32m3440\u001B[39m \u001B[38;5;28;01masync\u001B[39;00m \u001B[38;5;28;01mfor\u001B[39;00m _ \u001B[38;5;129;01min\u001B[39;00m runner.atick(\n\u001B[32m 3441\u001B[39m [t \u001B[38;5;28;01mfor\u001B[39;00m t \u001B[38;5;129;01min\u001B[39;00m loop.tasks.values() \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;129;01mnot\u001B[39;00m t.writes],\n\u001B[32m 3442\u001B[39m timeout=\u001B[38;5;28mself\u001B[39m.step_timeout,\n\u001B[32m 3443\u001B[39m get_waiter=get_waiter,\n\u001B[32m 3444\u001B[39m schedule_task=loop.aaccept_push,\n\u001B[32m 3445\u001B[39m ):\n\u001B[32m 3446\u001B[39m \u001B[38;5;66;03m# emit output\u001B[39;00m\n\u001B[32m 3447\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m o \u001B[38;5;129;01min\u001B[39;00m _output(\n\u001B[32m 3448\u001B[39m stream_mode,\n\u001B[32m 3449\u001B[39m print_mode,\n\u001B[32m (...)\u001B[39m\u001B[32m 3455\u001B[39m _state_mapper,\n\u001B[32m 3456\u001B[39m ):\n\u001B[32m 3457\u001B[39m \u001B[38;5;28;01myield\u001B[39;00m o\n",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_runner.py:396\u001B[39m, in \u001B[36mPregelRunner.atick\u001B[39m\u001B[34m(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)\u001B[39m\n\u001B[32m 394\u001B[39m scheduled_error_handler = \u001B[38;5;28;01mFalse\u001B[39;00m\n\u001B[32m 395\u001B[39m \u001B[38;5;28;01mtry\u001B[39;00m:\n\u001B[32m--> \u001B[39m\u001B[32m396\u001B[39m \u001B[38;5;28;01mawait\u001B[39;00m arun_with_retry(\n\u001B[32m 397\u001B[39m t,\n\u001B[32m 398\u001B[39m retry_policy,\n\u001B[32m 399\u001B[39m stream=\u001B[38;5;28mself\u001B[39m.use_astream,\n\u001B[32m 400\u001B[39m configurable={\n\u001B[32m 401\u001B[39m CONFIG_KEY_CALL: partial(\n\u001B[32m 402\u001B[39m _acall,\n\u001B[32m 403\u001B[39m weakref.ref(t),\n\u001B[32m 404\u001B[39m stream=\u001B[38;5;28mself\u001B[39m.use_astream,\n\u001B[32m 405\u001B[39m retry_policy=retry_policy,\n\u001B[32m 406\u001B[39m futures=weakref.ref(futures),\n\u001B[32m 407\u001B[39m schedule_task=schedule_task,\n\u001B[32m 408\u001B[39m submit=\u001B[38;5;28mself\u001B[39m.submit,\n\u001B[32m 409\u001B[39m loop=loop,\n\u001B[32m 410\u001B[39m ),\n\u001B[32m 411\u001B[39m },\n\u001B[32m 412\u001B[39m )\n\u001B[32m 413\u001B[39m \u001B[38;5;28mself\u001B[39m.commit(t, \u001B[38;5;28;01mNone\u001B[39;00m)\n\u001B[32m 414\u001B[39m \u001B[38;5;28;01mexcept\u001B[39;00m \u001B[38;5;167;01mException\u001B[39;00m \u001B[38;5;28;01mas\u001B[39;00m exc:\n",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_retry.py:745\u001B[39m, in \u001B[36marun_with_retry\u001B[39m\u001B[34m(task, retry_policy, stream, match_cached_writes, configurable)\u001B[39m\n\u001B[32m 743\u001B[39m \u001B[38;5;28;01mbreak\u001B[39;00m\n\u001B[32m 744\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28;01mawait\u001B[39;00m task.proc.ainvoke(task.input, config)\n\u001B[32m--> \u001B[39m\u001B[32m745\u001B[39m result = \u001B[38;5;28;01mawait\u001B[39;00m _arun_with_timeout(\n\u001B[32m 746\u001B[39m task, config, resolved_timeout, attempt_ctx, stream=stream\n\u001B[32m 747\u001B[39m )\n\u001B[32m 748\u001B[39m _finish_timed_attempt(config, attempt_ctx)\n\u001B[32m 749\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m stream:\n\u001B[32m 750\u001B[39m \u001B[38;5;66;03m# if successful, end\u001B[39;00m\n",
"\u001B[36mFile \u001B[39m\u001B[32m~\\Desktop\\python-develop\\lotteryServer\\.venv\\Lib\\site-packages\\langgraph\\pregel\\_retry.py:496\u001B[39m, in \u001B[36m_arun_with_timeout\u001B[39m\u001B[34m(task, config, timeout, attempt_ctx, stream)\u001B[39m\n\u001B[32m 494\u001B[39m bg.cancel()\n\u001B[32m 495\u001B[39m bg.add_done_callback(_drain_cancelled)\n\u001B[32m--> \u001B[39m\u001B[32m496\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m NodeTimeoutError(\n\u001B[32m 497\u001B[39m task.name,\n\u001B[32m 498\u001B[39m elapsed,\n\u001B[32m 499\u001B[39m kind=kind,\n\u001B[32m 500\u001B[39m idle_timeout=idle_timeout_s,\n\u001B[32m 501\u001B[39m run_timeout=run_timeout_s,\n\u001B[32m 502\u001B[39m ) \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mexc\u001B[39;00m\n\u001B[32m 503\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mRuntimeError\u001B[39;00m(\n\u001B[32m 504\u001B[39m \u001B[33mf\u001B[39m\u001B[33m\"\u001B[39m\u001B[38;5;132;01m{\u001B[39;00mkind\u001B[38;5;132;01m}\u001B[39;00m\u001B[33m timeout watchdog completed without raising TimeoutError\u001B[39m\u001B[33m\"\u001B[39m\n\u001B[32m 505\u001B[39m )\n\u001B[32m 506\u001B[39m \u001B[38;5;28;01mraise\u001B[39;00m \u001B[38;5;167;01mRuntimeError\u001B[39;00m(\u001B[33m\"\u001B[39m\u001B[33mtimeout wait completed without task or watchdog\u001B[39m\u001B[33m\"\u001B[39m)\n",
"\u001B[31mNodeTimeoutError\u001B[39m: Node 'node_a' exceeded its run timeout of 1.000s (elapsed: 1.007s).",
"During task with name 'node_a' and id 'b35c22ae-a4be-38ef-f687-20b76ea2dd87'"
]
}
],
"execution_count": 25
}
],
"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
}

75
lg/steam_out.ipynb Normal file
View File

@@ -0,0 +1,75 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"from langgraph.graph import StateGraph, START, END, MessagesState\n",
"from langchain.messages import HumanMessage\n",
"from langchain_deepseek import ChatDeepSeek\n",
"\n",
"from core.config.settings import settings\n",
"\n",
"model = ChatDeepSeek(\n",
" model=\"deepseek-v4-flash\",\n",
" api_key=settings.DEEPSEEK_API_KEY,\n",
" extra_body={\n",
" \"thinking\": {\n",
" \"type\": \"disabled\"\n",
" }\n",
" }\n",
")\n",
"\n",
"def llm_node(state: MessagesState) -> MessagesState:\n",
" messages = state[\"messages\"]\n",
" response = model.invoke(messages)\n",
"\n",
" return {\n",
" \"messages\": [response],\n",
" }\n",
"\n",
"builder = StateGraph(state_schema=MessagesState)\n",
"builder.add_node(\"llm_node\", llm_node)\n",
"builder.add_edge(START, \"llm_node\")\n",
"builder.add_edge(\"llm_node\", END)\n",
"\n",
"graph = builder.compile()\n",
"\n",
"# 使用流式输出\n",
"for chunk in graph.stream(\n",
" {\n",
" \"messages\":[HumanMessage(content=\"你好!\")]\n",
" },\n",
" stream_mode=[\"values\",\"messages\"],\n",
"):\n",
" print(chunk)"
]
}
],
"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
}

33
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()
logger.info("服务启动创建携程初始化chef_agent任务")
# 异步初始化chef_agent
chef_agent.initialize()
logger.info("服务启动完成")
try:
yield # 此处交出控制权,服务开始运行 yield # 此处交出控制权,服务开始运行
finally:
# ========== 关闭时执行 ========== # ========== 关闭时执行 ==========
logger.info("服务关闭,释放资源") logger.info("服务关闭,释放资源")
chef_agent.close()
scheduler.shutdown() 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,24 @@
[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",
"ipython>=9.17.1",
"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 +26,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)

2397
uv.lock generated

File diff suppressed because it is too large Load Diff