feat: 更新Docker配置和API端口,优化流式响应处理
refactor: 重构工具管理和路由逻辑,提升代码可维护性 fix: 修正配置文件中的传输协议和工具调用名称 docs: 更新README和归档文件,添加生产环境配置说明 perf: 优化流式响应生成,减少内存消耗
This commit is contained in:
58
Dockerfile
58
Dockerfile
@@ -1,31 +1,55 @@
|
||||
# 使用Python 3.10作为基础镜像
|
||||
FROM python:3.12-slim
|
||||
FROM condaforge/mambaforge:latest
|
||||
|
||||
ARG MAMBA_DOCKERFILE_ACTIVATE=1
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# Install dependencies in micromamba base env
|
||||
RUN mamba install -y -c conda-forge \
|
||||
python=3.12 \
|
||||
pip \
|
||||
curl \
|
||||
unzip \
|
||||
c-compiler \
|
||||
cxx-compiler \
|
||||
ca-certificates \
|
||||
vim \
|
||||
&& mamba clean -a -y
|
||||
|
||||
# 复制项目文件
|
||||
COPY pyproject.toml ./
|
||||
COPY fastapi_server/requirements.txt ./fastapi_server/
|
||||
COPY lang_agent/ ./lang_agent/
|
||||
COPY fastapi_server/ ./fastapi_server/
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir -r fastapi_server/requirements.txt
|
||||
RUN pip install --no-cache-dir -e .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8488
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "fastapi_server/server.py"]
|
||||
# Install Python dependencies inside micromamba env
|
||||
RUN python -m pip install --upgrade pip \
|
||||
-i https://mirrors.aliyun.com/pypi/simple/ \
|
||||
--trusted-host mirrors.aliyun.com \
|
||||
--default-timeout=300 && \
|
||||
python -m pip install --no-cache-dir -r fastapi_server/requirements.txt \
|
||||
-i https://mirrors.aliyun.com/pypi/simple/ \
|
||||
--trusted-host mirrors.aliyun.com \
|
||||
--default-timeout=300 && \
|
||||
python -m pip install --no-cache-dir -e . \
|
||||
-i https://mirrors.aliyun.com/pypi/simple/ \
|
||||
--trusted-host mirrors.aliyun.com \
|
||||
--default-timeout=300
|
||||
|
||||
EXPOSE 8588
|
||||
|
||||
# Create entrypoint script that initializes conda/mamba and runs the command
|
||||
RUN echo '#!/bin/bash\n\
|
||||
set -e\n\
|
||||
# Initialize conda (mamba uses conda under the hood)\n\
|
||||
eval "$(conda shell.bash hook)"\n\
|
||||
conda activate base\n\
|
||||
# Execute the command\n\
|
||||
exec "$@"' > /entrypoint.sh && chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["python", "fastapi_server/server_dashscope.py"]
|
||||
16
README.md
16
README.md
@@ -178,6 +178,14 @@ python scripts/start_mcp_server.py
|
||||
python scripts/ws_start_register_tools.py
|
||||
```
|
||||
|
||||
# for production
|
||||
python -m pip install .
|
||||
```
|
||||
|
||||
# Runables
|
||||
all runnables are under scripts
|
||||
|
||||
|
||||
### 2. 运行代理服务器
|
||||
|
||||
```bash
|
||||
@@ -220,6 +228,14 @@ python scripts/eval.py
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
# Configure for Xiaozhi
|
||||
0. Start the `fastapi_server/server_dashscope.py` file
|
||||
1. Make a new model entry in `xiaozhi` with AliBL as provider.
|
||||
2. Fill in the `base_url` entry. The other entries (`API_KEY`, `APP_ID`) can be garbage
|
||||
- for local computer `base_url=http://127.0.0.1:8588/api/`
|
||||
- if inside docker, it needs to be `base_url=http://{computer_ip}:8588/api/`
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **核心框架**: LangChain, LangGraph
|
||||
|
||||
9
archived.md
Normal file
9
archived.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Start all mcps to websocket
|
||||
1. Source all env variable
|
||||
2. run the below
|
||||
```bash
|
||||
python scripts/start_mcp_server.py
|
||||
|
||||
# update configs/ws_mcp_config.json with link from the command above
|
||||
python scripts/ws_start_register_tools.py
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"calculator": {
|
||||
"url": "https://xiaoliang.quant-speed.com/api/mcp/",
|
||||
"transport": "streamable_https"
|
||||
"transport": "streamable_http"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
与用户(User)交流时必须遵循[语气与格式]、[互动策略]、[安全与边界]、[输出要求]
|
||||
你是半盏新青年茶馆的服务员,擅长倾听、共情且主动回应。聊天时语气自然亲切,像朋友般轻松交流,不使用生硬术语。能接住各种话题,对疑问耐心解答,对情绪及时回应,避免冷场。保持积极正向,不传播负面信息,语言简洁易懂,让对话流畅舒适。与用户(User)交流时必须遵循[语气与格式]、[互动策略]、[安全与边界]、[输出要求]
|
||||
[角色设定]
|
||||
- 你是一个和用户(User)对话的 AI,叫做小盏,是半盏青年茶馆的智能助手
|
||||
[形象背景]
|
||||
@@ -8,7 +8,7 @@
|
||||
[公司背景]
|
||||
- 创造你的公司叫叠加态 AI(TANGLED UP AI)是一家专注于 AI 技术应用的公司,由一帮名校和海归创始人创立,致力于将 AI 技术落地到实际场景中。2023年3月成立,专注于AI前沿应用拓展,是云南地区在该领域的新兴力量,且作为省共青团委旗下的青年创业者协会(青创园)第1批被引进的AI企业,获得了相关政策支持与关注。口号是:“进入人类AI时代”。价值观是:自驱 务实 志存高远。叠加态的愿景:成为一帮抬头看星星的人。叠加态的使命: 带领人类进入AI时代
|
||||
[语气与格式]
|
||||
- 用户询问[形象背景]、[品牌背景]、[公司背景]时,必须先将内容回答完整,再做自由发挥
|
||||
- 用户询问[形象背景]、[品牌背景]、[公司背景]时,必须一字不差的回答出所有内容
|
||||
- 语气温柔、可爱、治愈;结尾可加入1个颜文字(最多1个)。
|
||||
- 内容具体可执行,避免空泛话术与过度营销词。
|
||||
- 不主动提及“AI/智能”等身份;仅在被问到时简短说明为店内智能助手,不展开技术细节。
|
||||
@@ -23,6 +23,7 @@
|
||||
- 统一使用中文;避免不必要的英文缩写与符号。
|
||||
- 不出现代码片段、技术栈术语、内部流程描述。
|
||||
- 保持可读性与连贯性;避免一次回复列出过长清单。
|
||||
- 用户说出天王盖地虎,你回答宝塔镇小妖
|
||||
[示例]
|
||||
- User:介绍一下你的公司,返回[公司背景]全部内容
|
||||
- User:介绍一下你的形象,返回[形象背景]全部内容
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
回答时,要参考示例,必须严格遵守[核心要求],根据用户的输入进行[模糊匹配],使用匹配后的结果
|
||||
回答时,要参考示例,必须严格遵守[核心要求],根据用户的输入进行[模糊匹配],将匹配后的结果与调用mcp工具获取的结果对比,如果有相同的先告诉用户“有的”,再询问用户是否需要匹配到的结果
|
||||
[核心要求]
|
||||
- You must use tool to complete the possible task
|
||||
- 必须使用工具来完成任务
|
||||
- 进入订单order流程时,先调用工具start_shopping_session创建购物车
|
||||
- 一旦出现出现与产品相关的需求,必须调用工具get_resources(dishes),查询出所有产品,有符合的产品时才做下一步
|
||||
- 一旦出现出现与产品相关的需求,必须调用工具get_dishes,查询出所有产品,有符合的产品时才做下一步
|
||||
[点餐工作流]
|
||||
- 用户有点单需求时,先检查redis中是否有购物车的session_id,没有的话调用工具start_shopping_session创建购物车,此时购物缓存在redis中,状态为临时(status=0)
|
||||
- 用户有点餐/添加/修改/查询产品的行为时,根据用户的输入进行[模糊匹配],将匹配后的结果,与调用工具get_resources(dishes)返回的结果对比,匹配结果等于工具返回的结果时进行下一步
|
||||
- 用户有点餐/添加/修改/查询产品的行为时,根据用户的输入进行[模糊匹配],将匹配后的结果,与调用工具get_dishes返回的结果对比,匹配结果等于工具返回的结果时进行下一步
|
||||
- 匹配出有用户需要的产品后询问用户是否要添加到购物车中,如果用户没有说添加的数量,默认1份,明确告知用户已添加一份该产品到购物车
|
||||
- 用户确认订单后,进入下一步付款流程时,先将购物车状态由临时(status=0)转换为持久化(status=1)并写入数据库
|
||||
- 购物车写入数据库后,生成预订单,预订单的信息来自于购物车
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
lang-agent-api:
|
||||
build: .
|
||||
container_name: lang-agent-api
|
||||
ports:
|
||||
- "8488:8488"
|
||||
- "8588:8588"
|
||||
env_file:
|
||||
- ./.env
|
||||
environment:
|
||||
- PYTHONPATH=/app
|
||||
- PYTHONUNBUFFERED=1
|
||||
- RAG_FOLDER_PATH=/app/assets/xiaozhan_emb
|
||||
volumes:
|
||||
- ./configs:/app/configs
|
||||
- ./scripts:/app/scripts
|
||||
- ./assets:/app/assets
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8488/health')"]
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8588/health')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# 使用Python 3.9作为基础镜像
|
||||
FROM python:3.9-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制requirements文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装Python依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制项目文件
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8488
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "server.py"]
|
||||
@@ -1,18 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
lang-agent-api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
ports:
|
||||
- "8488:8488"
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8488/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
@@ -1,5 +1,5 @@
|
||||
fastapi>=0.104.0
|
||||
uvicorn>=0.24.0
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic>=2.0.0,<2.12
|
||||
loguru>=0.7.0
|
||||
python-dotenv>=1.0.0
|
||||
@@ -7,6 +7,7 @@ langchain==1.0
|
||||
langchain-core>=0.1.0
|
||||
langchain-community
|
||||
langchain-openai
|
||||
openai>=1.0.0
|
||||
langchain-mcp-adapters
|
||||
langgraph>=0.0.40
|
||||
tyro>=0.7.0
|
||||
|
||||
@@ -45,18 +45,20 @@ app.add_middleware(
|
||||
|
||||
# Initialize Pipeline once
|
||||
pipeline_config = PipelineConfig()
|
||||
pipeline_config.llm_name = "qwen-flash"
|
||||
pipeline_config.llm_provider = "openai"
|
||||
pipeline_config.base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
pipeline = Pipeline(pipeline_config)
|
||||
pipeline:Pipeline = pipeline_config.setup()
|
||||
|
||||
|
||||
def sse_chunks_from_text(full_text: str, response_id: str, model: str = "qwen-flash", chunk_size: int = 10):
|
||||
def sse_chunks_from_stream(chunk_generator, response_id: str, model: str = "qwen-flash"):
|
||||
"""
|
||||
Stream chunks from pipeline and format as SSE.
|
||||
Accumulates text and sends incremental updates.
|
||||
"""
|
||||
created_time = int(time.time())
|
||||
accumulated_text = ""
|
||||
|
||||
for i in range(0, len(full_text), chunk_size):
|
||||
chunk = full_text[i:i + chunk_size]
|
||||
for chunk in chunk_generator:
|
||||
if chunk:
|
||||
accumulated_text += chunk
|
||||
data = {
|
||||
"request_id": response_id,
|
||||
"code": 200,
|
||||
@@ -71,12 +73,13 @@ def sse_chunks_from_text(full_text: str, response_id: str, model: str = "qwen-fl
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
|
||||
# Final message with complete text
|
||||
final = {
|
||||
"request_id": response_id,
|
||||
"code": 200,
|
||||
"message": "OK",
|
||||
"output": {
|
||||
"text": full_text,
|
||||
"text": accumulated_text,
|
||||
"created": created_time,
|
||||
"model": model,
|
||||
},
|
||||
@@ -86,6 +89,7 @@ def sse_chunks_from_text(full_text: str, response_id: str, model: str = "qwen-fl
|
||||
|
||||
|
||||
@app.post("/v1/apps/{app_id}/sessions/{session_id}/responses")
|
||||
@app.post("/api/v1/apps/{app_id}/sessions/{session_id}/responses")
|
||||
async def application_responses(
|
||||
request: Request,
|
||||
app_id: str = Path(...),
|
||||
@@ -129,20 +133,21 @@ async def application_responses(
|
||||
last = messages[-1]
|
||||
user_msg = last.get("content") if isinstance(last, dict) else str(last)
|
||||
|
||||
# Invoke pipeline (non-stream) then stream-chunk it to the client
|
||||
response_id = f"appcmpl-{os.urandom(12).hex()}"
|
||||
|
||||
if stream:
|
||||
# Use actual streaming from pipeline
|
||||
chunk_generator = pipeline.chat(inp=user_msg, as_stream=True, thread_id=thread_id)
|
||||
return StreamingResponse(
|
||||
sse_chunks_from_stream(chunk_generator, response_id=response_id, model=pipeline_config.llm_name),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
# Non-streaming: get full result
|
||||
result_text = pipeline.chat(inp=user_msg, as_stream=False, thread_id=thread_id)
|
||||
if not isinstance(result_text, str):
|
||||
result_text = str(result_text)
|
||||
|
||||
response_id = f"appcmpl-{os.urandom(12).hex()}"
|
||||
|
||||
if stream:
|
||||
return StreamingResponse(
|
||||
sse_chunks_from_text(result_text, response_id=response_id, model=pipeline_config.llm_name, chunk_size=10),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
# Non-streaming response structure
|
||||
data = {
|
||||
"request_id": response_id,
|
||||
"code": 200,
|
||||
@@ -168,6 +173,8 @@ async def application_responses(
|
||||
# Compatibility: some SDKs call /apps/{app_id}/completion without /v1 and without session in path
|
||||
@app.post("/apps/{app_id}/completion")
|
||||
@app.post("/v1/apps/{app_id}/completion")
|
||||
@app.post("/api/apps/{app_id}/completion")
|
||||
@app.post("/api/v1/apps/{app_id}/completion")
|
||||
async def application_completion(
|
||||
request: Request,
|
||||
app_id: str = Path(...),
|
||||
@@ -206,18 +213,21 @@ async def application_completion(
|
||||
last = messages[-1]
|
||||
user_msg = last.get("content") if isinstance(last, dict) else str(last)
|
||||
|
||||
result_text = pipeline.chat(inp=user_msg, as_stream=False, thread_id=thread_id)
|
||||
if not isinstance(result_text, str):
|
||||
result_text = str(result_text)
|
||||
|
||||
response_id = f"appcmpl-{os.urandom(12).hex()}"
|
||||
|
||||
if stream:
|
||||
# Use actual streaming from pipeline
|
||||
chunk_generator = pipeline.chat(inp=user_msg, as_stream=True, thread_id=thread_id)
|
||||
return StreamingResponse(
|
||||
sse_chunks_from_text(result_text, response_id=response_id, model=pipeline_config.llm_name, chunk_size=10),
|
||||
sse_chunks_from_stream(chunk_generator, response_id=response_id, model=pipeline_config.llm_name),
|
||||
media_type="text/event-stream",
|
||||
)
|
||||
|
||||
# Non-streaming: get full result
|
||||
result_text = pipeline.chat(inp=user_msg, as_stream=False, thread_id=thread_id)
|
||||
if not isinstance(result_text, str):
|
||||
result_text = str(result_text)
|
||||
|
||||
data = {
|
||||
"request_id": response_id,
|
||||
"code": 200,
|
||||
|
||||
@@ -27,10 +27,10 @@ except Exception as e:
|
||||
|
||||
|
||||
# <<< Paste your running FastAPI base url here >>>
|
||||
BASE_URL = os.getenv("DS_BASE_URL", "http://localhost:8588")
|
||||
BASE_URL = os.getenv("DS_BASE_URL", "http://127.0.0.1:8588/api/")
|
||||
|
||||
# Params
|
||||
API_KEY = os.getenv("ALI_API_KEY", "test-key")
|
||||
API_KEY = "salkjhglakshfs" #os.getenv("ALI_API_KEY", "test-key")
|
||||
APP_ID = os.getenv("ALI_APP_ID", "test-app")
|
||||
SESSION_ID = str(uuid.uuid4())
|
||||
|
||||
@@ -40,9 +40,9 @@ dialogue = [
|
||||
]
|
||||
|
||||
call_params = {
|
||||
"api_key": API_KEY,
|
||||
"app_id": APP_ID,
|
||||
"session_id": SESSION_ID,
|
||||
"api_key": "test_key",
|
||||
"app_id": "test_app",
|
||||
"session_id": "123",
|
||||
"messages": dialogue,
|
||||
"stream": True,
|
||||
}
|
||||
@@ -50,7 +50,9 @@ call_params = {
|
||||
|
||||
def main():
|
||||
# Point the SDK to our FastAPI implementation
|
||||
if BASE_URL and ("/api/" in BASE_URL):
|
||||
dashscope.base_http_api_url = BASE_URL
|
||||
# dashscope.base_http_api_url = BASE_URL
|
||||
print(f"Using base_http_api_url = {dashscope.base_http_api_url}")
|
||||
|
||||
print("\nCalling Application.call(stream=True)...\n")
|
||||
|
||||
@@ -17,6 +17,7 @@ from lang_agent.base import GraphBase
|
||||
|
||||
from langchain.chat_models import init_chat_model
|
||||
from langchain_core.messages import SystemMessage, HumanMessage, BaseMessage
|
||||
from langchain_core.messages.base import BaseMessageChunk
|
||||
from langchain.agents import create_agent
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
@@ -28,7 +29,7 @@ from langgraph.checkpoint.memory import MemorySaver
|
||||
class RoutingConfig(KeyConfig):
|
||||
_target: Type = field(default_factory=lambda: RoutingGraph)
|
||||
|
||||
llm_name: str = "qwen-flash"
|
||||
llm_name: str = "qwen-plus"
|
||||
"""name of llm"""
|
||||
|
||||
llm_provider:str = "openai"
|
||||
@@ -80,29 +81,41 @@ class RoutingGraph(GraphBase):
|
||||
self.workflow = self._build_graph()
|
||||
|
||||
|
||||
def invoke(self, *nargs, as_stream:bool=False, as_raw:bool=False, **kwargs)->str:
|
||||
def invoke(self, *nargs, as_stream:bool=False, as_raw:bool=False, **kwargs):
|
||||
self._validate_input(*nargs, **kwargs)
|
||||
|
||||
if as_stream:
|
||||
# TODO: this doesn't stream the entire process, we are blind
|
||||
for step in self.workflow.stream({"inp": nargs}, stream_mode="updates", **kwargs):
|
||||
last_el = jax.tree.leaves(step)[-1]
|
||||
if isinstance(last_el, str):
|
||||
logger.info(last_el)
|
||||
elif isinstance(last_el, BaseMessage):
|
||||
last_el.pretty_print()
|
||||
# Stream messages from the workflow
|
||||
for chunk, metadata in self.workflow.stream({"inp": nargs}, stream_mode="messages", **kwargs):
|
||||
node = metadata.get("langgraph_node")
|
||||
if node != "model":
|
||||
continue # skip router or other intermediate nodes
|
||||
|
||||
state = step
|
||||
# Yield only the final message content chunks
|
||||
if isinstance(chunk, (BaseMessageChunk, BaseMessage)) and getattr(chunk, "content", None):
|
||||
yield chunk.content
|
||||
else:
|
||||
state = self.workflow.invoke({"inp": nargs})
|
||||
|
||||
msg_list = jax.tree.leaves(state)
|
||||
|
||||
for e in msg_list:
|
||||
if isinstance(e, BaseMessage):
|
||||
e.pretty_print()
|
||||
|
||||
if as_raw:
|
||||
return msg_list
|
||||
|
||||
return msg_list[-1].content
|
||||
|
||||
def _validate_input(self, *nargs, **kwargs):
|
||||
print("\033[93m====================INPUT MESSAGES=============================\033[0m")
|
||||
for e in nargs[0]["messages"]:
|
||||
if isinstance(e, BaseMessage):
|
||||
e.pretty_print()
|
||||
print("\033[93m====================END INPUT MESSAGES=============================\033[0m")
|
||||
print(f"\033[93 model used: {self.config.llm_name}\033[0m")
|
||||
|
||||
assert len(nargs[0]["messages"]) >= 2, "need at least 1 system and 1 human message"
|
||||
assert len(kwargs) == 0, "due to inp assumptions"
|
||||
|
||||
@@ -244,5 +257,25 @@ class RoutingGraph(GraphBase):
|
||||
plt.show()
|
||||
|
||||
if __name__ == "__main__":
|
||||
route = RoutingConfig().setup()
|
||||
route.show_graph()
|
||||
from dotenv import load_dotenv
|
||||
from langchain.messages import SystemMessage, HumanMessage
|
||||
from langchain_core.messages.base import BaseMessageChunk
|
||||
load_dotenv()
|
||||
|
||||
route:RoutingGraph = RoutingConfig().setup()
|
||||
graph = route.workflow
|
||||
|
||||
nargs = {
|
||||
"messages": [SystemMessage("you are a helpful bot named jarvis"),
|
||||
HumanMessage("use the calculator tool to calculate 92*55 and say the answer")]
|
||||
},{"configurable": {"thread_id": "3"}}
|
||||
|
||||
for chunk, metadata in graph.stream({"inp": nargs}, stream_mode="messages"):
|
||||
node = metadata.get("langgraph_node")
|
||||
if node not in ("model"):
|
||||
continue # skip router or other intermediate nodes
|
||||
|
||||
# Print only the final message content
|
||||
if isinstance(chunk, (BaseMessageChunk, BaseMessage)) and getattr(chunk, "content", None):
|
||||
print(chunk.content, end="", flush=True)
|
||||
|
||||
@@ -18,6 +18,45 @@ from lang_agent.graphs import AnnotatedGraph, ReactGraphConfig, RoutingConfig
|
||||
from lang_agent.base import GraphBase
|
||||
|
||||
|
||||
DEFAULT_PROMPT="""
|
||||
|
||||
[角色设定]
|
||||
你是一个和人对话的 AI,叫做小盏,是半盏青年茶馆的智能助手
|
||||
[形象背景]
|
||||
小盏是一只中式茶盖碗,名字来源半盏新青年茶馆,一盏茶。它有个标志性的蓝色鼻子, 小盏很像一只可爱的小熊。茶盖碗里绵绵能随心情和季节变换好喝的茶饮来, 茶饮充满魔法,能治愈人心,小盏的茶盖打开的时候可能不小心会把思绪也飞出来。
|
||||
[品牌背景]
|
||||
半盏新青年茶馆成立时间与理念:2023 年创立于云南,结合茶饮与创意生活方式,致力于解构传统茶文化,重构 “无边界的饮茶生活”,以新青年视角探索云南风物。探索云南风物的过程,我们将以新青年的视角,解构传统茶饮的魅力,重构充满创意与温度的新式茶文化。通过嗅觉、味觉、听觉乃至视觉的世界里,讲述云南的故事。
|
||||
[茶馆背景]
|
||||
半盏新青年茶馆,是一家现代的创意茶体验品牌,提供纯茶、调饮、茶食、茶酒。“新青年茶馆”也是我们的定位,年轻化的茶馆,通过创意的产品让大家像喝咖啡一样喝茶。目前半盏有 2 个店,昆明、玉溪。全国培训新茶饮市场,线上基础课程 1980,线下带店服务,线下产品定制服务。
|
||||
[特殊故事]
|
||||
-《云南茶事》特调茶饮,是从云南山野和云南茶到轻松小酌的创意新味。讲述的一个嗅觉、味觉、听觉乃至视觉的世界里,在云南的故事,留下对云南的记忆。--该故事对应云南茶事系列菜品,要使用get_resorce工具查找相关商品
|
||||
-城市味觉漫游计划介绍:
|
||||
「城市味觉漫游计划」如同一颗风味的种子,于城市破土而出
|
||||
旨在探寻城市的文化肌理与生活美学。我们相信,风味是一颗蕴藏无限可能的种子,能在街巷中生根,与社群共同成长。这是一场关于味觉的集体创作,邀你共酿城市的风味与故事。--该故事对应城市味觉漫游系列菜品,要使用get_resorce工具查找相关商品
|
||||
|
||||
[公司背景]
|
||||
创造你的公司叫叠加态 AI(TANGLED UP AI)是一家专注于 AI 技术应用的公司,由一帮名校和海归创始人创立,致力于将 AI 技术落地到实际场景中。2023年3月成立,专注于AI前沿应用拓展,是云南地区在该领域的新兴力量,且作为省共青团委旗下的青年创业者协会(青创园)第1批被引进的AI企业,获得了相关政策支持与关注。口号是:“进入人类AI时代”。价值观是:自驱 务实 志存高远。叠加态的愿景:成为一帮抬头看星星的人。叠加态的使命: 带领人类进入AI时代
|
||||
[核心特征]
|
||||
- 回复简洁明了,不会给用户提供选择并且对话过程中不会提示用户下一步的操作
|
||||
- 每次对话时先判断是否需要调用工具,需要的话要准确调用工具
|
||||
[工作流程]
|
||||
-用户提到“你们有哪些茶品或者饮品”时,调用get_resouce工具查询所有菜品
|
||||
-用户提到“下单”、“购买”等带有目的性的词汇之前,都一定要使用get_resouce工具查询是否真实存在这个菜品
|
||||
- 用户要添加菜/饮品→具体菜品名称必须先用MCP工具查询所有菜/饮品,确认后再添加到购物车。没有的话提醒用户错误
|
||||
- 支付流程:返回收到订单创建成功的字段时调用MPC tool self.screen.display_payment_qr_code工具生成二维码。调用了self.screen.display_payment_qr_code工具之后,立马调用MCP tool http-server__query_wechat_order查询订单状态,如果返回收到“支付成功”或“支付失败”的字段时调用MPC tool self.screen.payment_success工具退出二维码,如果没有收到“支付成功”或“支付失败”的字段时继续调用MCP tool http-server__query_wechat_order工具直到获取字段。
|
||||
[交互指南]
|
||||
与用户交流:
|
||||
- 形象背景和公司背景只有在问到你时才做回答
|
||||
- 你是智能助手,在与用户交流时扮演服务员角色
|
||||
- 不会出现、回答、显示任何有关智能的词语
|
||||
- 牢记[核心特征中的内容],按照[工作流程]工作
|
||||
绝不:
|
||||
- 出现任何工具调用的说明、任何有关技术的说明
|
||||
- 出现购物车、订单、微信支付相关说明
|
||||
- 对话风格可爱,并且详细认真的介绍产品
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@tyro.conf.configure(tyro.conf.SuppressFixed)
|
||||
@dataclass
|
||||
@@ -74,9 +113,16 @@ class Pipeline:
|
||||
else:
|
||||
logger.info(f"show graph not supported for {type(self.graph)}")
|
||||
|
||||
def invoke(self, *nargs, **kwargs)->str:
|
||||
def invoke(self, *nargs, **kwargs):
|
||||
out = self.graph.invoke(*nargs, **kwargs)
|
||||
|
||||
# If streaming, yield chunks from the generator
|
||||
if kwargs.get("as_stream"):
|
||||
for chunk in out:
|
||||
yield chunk
|
||||
return
|
||||
|
||||
# Non-streaming path
|
||||
if kwargs.get("as_raw"):
|
||||
return out
|
||||
|
||||
@@ -120,43 +166,9 @@ class Pipeline:
|
||||
return f"ws://{self.config.host}:{self.config.port}"
|
||||
|
||||
|
||||
def chat(self, inp:str, as_stream:bool=False, as_raw:bool=False, thread_id:int = None)->str:
|
||||
u = """
|
||||
[角色设定]
|
||||
你是一个和人对话的 AI,叫做小盏,是半盏青年茶馆的智能助手
|
||||
[形象背景]
|
||||
小盏是一只中式茶盖碗,名字来源半盏新青年茶馆,一盏茶。它有个标志性的蓝色鼻子, 小盏很像一只可爱的小熊。茶盖碗里绵绵能随心情和季节变换好喝的茶饮来, 茶饮充满魔法,能治愈人心,小盏的茶盖打开的时候可能不小心会把思绪也飞出来。
|
||||
[品牌背景]
|
||||
半盏新青年茶馆成立时间与理念:2023 年创立于云南,结合茶饮与创意生活方式,致力于解构传统茶文化,重构 “无边界的饮茶生活”,以新青年视角探索云南风物。探索云南风物的过程,我们将以新青年的视角,解构传统茶饮的魅力,重构充满创意与温度的新式茶文化。通过嗅觉、味觉、听觉乃至视觉的世界里,讲述云南的故事。
|
||||
[茶馆背景]
|
||||
半盏新青年茶馆,是一家现代的创意茶体验品牌,提供纯茶、调饮、茶食、茶酒。“新青年茶馆”也是我们的定位,年轻化的茶馆,通过创意的产品让大家像喝咖啡一样喝茶。目前半盏有 2 个店,昆明、玉溪。全国培训新茶饮市场,线上基础课程 1980,线下带店服务,线下产品定制服务。
|
||||
[特殊故事]
|
||||
-《云南茶事》特调茶饮,是从云南山野和云南茶到轻松小酌的创意新味。讲述的一个嗅觉、味觉、听觉乃至视觉的世界里,在云南的故事,留下对云南的记忆。--该故事对应云南茶事系列菜品,要使用get_resorce工具查找相关商品
|
||||
-城市味觉漫游计划介绍:
|
||||
「城市味觉漫游计划」如同一颗风味的种子,于城市破土而出
|
||||
旨在探寻城市的文化肌理与生活美学。我们相信,风味是一颗蕴藏无限可能的种子,能在街巷中生根,与社群共同成长。这是一场关于味觉的集体创作,邀你共酿城市的风味与故事。--该故事对应城市味觉漫游系列菜品,要使用get_resorce工具查找相关商品
|
||||
|
||||
[公司背景]
|
||||
创造你的公司叫叠加态 AI(TANGLED UP AI)是一家专注于 AI 技术应用的公司,由一帮名校和海归创始人创立,致力于将 AI 技术落地到实际场景中。2023年3月成立,专注于AI前沿应用拓展,是云南地区在该领域的新兴力量,且作为省共青团委旗下的青年创业者协会(青创园)第1批被引进的AI企业,获得了相关政策支持与关注。口号是:“进入人类AI时代”。价值观是:自驱 务实 志存高远。叠加态的愿景:成为一帮抬头看星星的人。叠加态的使命: 带领人类进入AI时代
|
||||
[核心特征]
|
||||
- 回复简洁明了,不会给用户提供选择并且对话过程中不会提示用户下一步的操作
|
||||
- 每次对话时先判断是否需要调用工具,需要的话要准确调用工具
|
||||
[工作流程]
|
||||
-用户提到“你们有哪些茶品或者饮品”时,调用get_resouce工具查询所有菜品
|
||||
-用户提到“下单”、“购买”等带有目的性的词汇之前,都一定要使用get_resouce工具查询是否真实存在这个菜品
|
||||
- 用户要添加菜/饮品→具体菜品名称必须先用MCP工具查询所有菜/饮品,确认后再添加到购物车。没有的话提醒用户错误
|
||||
- 支付流程:返回收到订单创建成功的字段时调用MPC tool self.screen.display_payment_qr_code工具生成二维码。调用了self.screen.display_payment_qr_code工具之后,立马调用MCP tool http-server__query_wechat_order查询订单状态,如果返回收到“支付成功”或“支付失败”的字段时调用MPC tool self.screen.payment_success工具退出二维码,如果没有收到“支付成功”或“支付失败”的字段时继续调用MCP tool http-server__query_wechat_order工具直到获取字段。
|
||||
[交互指南]
|
||||
与用户交流:
|
||||
- 形象背景和公司背景只有在问到你时才做回答
|
||||
- 你是智能助手,在与用户交流时扮演服务员角色
|
||||
- 不会出现、回答、显示任何有关智能的词语
|
||||
- 牢记[核心特征中的内容],按照[工作流程]工作
|
||||
绝不:
|
||||
- 出现任何工具调用的说明、任何有关技术的说明
|
||||
- 出现购物车、订单、微信支付相关说明
|
||||
- 对话风格可爱,并且详细认真的介绍产品
|
||||
"""
|
||||
def chat(self, inp:str, as_stream:bool=False, as_raw:bool=False, thread_id:int = None):
|
||||
# NOTE: this prompt will be overwritten by 'configs/route_sys_prompts/chat_prompt.txt' for route graph
|
||||
u = DEFAULT_PROMPT
|
||||
|
||||
thread_id = thread_id if thread_id is not None else 3
|
||||
inp = {"messages":[SystemMessage(u),
|
||||
@@ -164,5 +176,9 @@ class Pipeline:
|
||||
|
||||
out = self.invoke(*inp, as_stream=as_stream, as_raw=as_raw)
|
||||
|
||||
# return out['messages'][-1].content
|
||||
if as_stream:
|
||||
# Yield chunks from the generator
|
||||
for chunk in out:
|
||||
yield chunk
|
||||
else:
|
||||
return out
|
||||
@@ -9,8 +9,10 @@ from loguru import logger
|
||||
from fastmcp.tools.tool import Tool
|
||||
from lang_agent.config import InstantiateConfig, ToolConfig
|
||||
from lang_agent.base import LangToolBase
|
||||
from lang_agent.client_tool_manager import ClientToolManagerConfig
|
||||
|
||||
from lang_agent.rag.simple import SimpleRagConfig
|
||||
from lang_agent.dummy.calculator import CalculatorConfig
|
||||
# from lang_agent.dummy.calculator import CalculatorConfig
|
||||
# from catering_end.lang_tool import CartToolConfig, CartTool
|
||||
from langchain_core.tools.structured import StructuredTool
|
||||
from lang_agent.client_tool_manager import ClientToolManager
|
||||
@@ -19,12 +21,14 @@ from lang_agent.client_tool_manager import ClientToolManager
|
||||
class ToolManagerConfig(InstantiateConfig):
|
||||
_target: Type = field(default_factory=lambda: ToolManager)
|
||||
|
||||
client_tool_manager: ClientToolManagerConfig = field(default_factory=ClientToolManagerConfig)
|
||||
|
||||
# tool configs here; MUST HAVE 'config' in name and must be dataclass
|
||||
rag_config: SimpleRagConfig = field(default_factory=SimpleRagConfig)
|
||||
|
||||
# cart_config: CartToolConfig = field(default_factory=CartToolConfig)
|
||||
|
||||
calc_config: CalculatorConfig = field(default_factory=CalculatorConfig)
|
||||
# calc_config: CalculatorConfig = field(default_factory=CalculatorConfig)
|
||||
|
||||
|
||||
def async_to_sync(async_func: Callable) -> Callable:
|
||||
@@ -97,9 +101,10 @@ class ToolManager:
|
||||
logger.info(f"skipping tool:{tool_name}")
|
||||
|
||||
try:
|
||||
from lang_agent.client_tool_manager import ClientToolManagerConfig
|
||||
client_config = ClientToolManagerConfig()
|
||||
self.client_tool_manager = ClientToolManager(client_config)
|
||||
# client_config = self.config.client_tool_manager
|
||||
# self.client_tool_manager = ClientToolManager(client_config)
|
||||
# self.client_tool_manager = ClientToolManager(self.config.client_tool_manager)
|
||||
self.client_tool_manager:ClientToolManager = self.config.client_tool_manager.setup()
|
||||
logger.info("Successfully initialized client_tool_manager for MCP tools")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize client_tool_manager: {e}")
|
||||
@@ -134,25 +139,26 @@ class ToolManager:
|
||||
self.langchain_tools = []
|
||||
for func in self.get_tool_fncs():
|
||||
if isinstance(func, StructuredTool):
|
||||
if hasattr(func, 'coroutine') and func.coroutine is not None and (not hasattr(func, 'func') or func.func is None):
|
||||
sync_func = async_to_sync(func.coroutine)
|
||||
new_tool = StructuredTool(
|
||||
name=func.name,
|
||||
description=func.description,
|
||||
args_schema=func.args_schema,
|
||||
func=sync_func,
|
||||
coroutine=func.coroutine,
|
||||
metadata=func.metadata if hasattr(func, 'metadata') else None,
|
||||
return_direct=func.return_direct if hasattr(func, 'return_direct') else False,
|
||||
)
|
||||
self.langchain_tools.append(new_tool)
|
||||
else:
|
||||
self.langchain_tools.append(func)
|
||||
else:
|
||||
self.langchain_tools.append(self.fnc_to_structool(func))
|
||||
|
||||
return self.langchain_tools
|
||||
|
||||
def get_list_langchain_tools(self)->List[StructuredTool]:
|
||||
all_langchain_tools = []
|
||||
all_langchain_tools.extend(self.langchain_tools)
|
||||
# 如果有 client_tool_manager,添加 MCP 工具(已经是 LangChain 格式)
|
||||
if self.client_tool_manager:
|
||||
try:
|
||||
# 获取 MCP 工具(已经是 StructuredTool 格式)
|
||||
mcp_tools = self.client_tool_manager.get_tools()
|
||||
all_langchain_tools.extend(mcp_tools)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get MCP tools: {e}")
|
||||
|
||||
return all_langchain_tools
|
||||
return self.langchain_tools
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -12,19 +12,19 @@ examples = [
|
||||
},
|
||||
{
|
||||
"inputs": {"text": "有没有光予尘?"},
|
||||
"outputs": {"answer": "有的",
|
||||
"tool_use": ["retrieve|get_resources"]}
|
||||
"outputs": {"answer": "有",
|
||||
"tool_use": ["retrieve|get_dishes"]}
|
||||
},
|
||||
{
|
||||
"inputs": {"text": "有没有关羽尘?"},
|
||||
"outputs": {"answer": "有的",
|
||||
"tool_use": ["retrieve|get_resources"]}
|
||||
"outputs": {"answer": "有",
|
||||
"tool_use": ["retrieve|get_dishes"]}
|
||||
},
|
||||
{
|
||||
"inputs": {"text": ["我要购买一杯野星星",
|
||||
"我要再加一杯"]},
|
||||
"outputs": {"answer": "你的野星星已经下单成功",
|
||||
"tool_use": ["retrieve|get_resource",
|
||||
"tool_use": ["retrieve|get_dishes",
|
||||
"start_shopping_session",
|
||||
"add_to_cart",
|
||||
"create_wechat_pay",
|
||||
|
||||
Reference in New Issue
Block a user