update a lot

This commit is contained in:
Wenjie Zhang 2025-03-25 05:40:07 +08:00
parent b7533a0111
commit dbf5000ac7
12 changed files with 1984 additions and 138 deletions

View File

@ -1,53 +1,39 @@
from src.agents.registry import BaseAgent
from src.agents.chatbot.graph import ChatbotAgent
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import AIMessageChunk, ToolMessage
from src.agents.chatbot import ChatbotAgent, ChatbotConfiguration
class AgentManager:
def __init__(self):
self.agents = {}
def add_agent(self, agent_id, agent):
def add_agent(self, agent_id, agent_class, configuration_class):
self.agents[agent_id] = {
"agent": agent,
"configuration": agent.configuration
"agent_class": agent_class,
"configuration_class": configuration_class
}
def agent_cli(agent: BaseAgent, config: RunnableConfig = None):
config = config or {}
if "configurable" not in config:
config["configurable"] = {}
def get_runnable_agent(self, agent_id, **kwargs):
agent_class = self.get_agent(agent_id)
configuration_class = self.get_configuration(agent_id)
configuration = configuration_class(**kwargs)
return agent_class(configuration)
while True:
user_input = input("\nUser: ")
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
def get_agent(self, agent_id):
return self.agents[agent_id]["agent_class"]
stream_flag = False
for msg, metadata in agent.stream_messages([{"role": "user", "content": user_input}], config):
if isinstance(msg, AIMessageChunk):
content = msg.content or msg.tool_calls
def get_configuration(self, agent_id):
return self.agents[agent_id]["configuration_class"]
if not content:
if stream_flag == True:
print()
stream_flag = False
continue
if stream_flag == False and content:
print(f"AI: {content}", end="", flush=True)
stream_flag = True
continue
agent_manager = AgentManager()
agent_manager.add_agent("chatbot", ChatbotAgent, ChatbotConfiguration)
elif content:
print(f"{content}", end="", flush=True)
__all__ = ["agent_manager"]
if isinstance(msg, ToolMessage):
print(f"Tool: {msg.content}")
def get_agents():
agent_manager = AgentManager()
agent_manager.add_agent("chatbot", ChatbotAgent())
return agent_manager.agents
if __name__ == "__main__":
agent = agent_manager.get_agent("chatbot")
conf = agent_manager.get_configuration("chatbot")
agent_info = {
"name": agent.name,
"description": agent.description,
}
print(agent_info)

View File

@ -1,3 +1,4 @@
from .graph import ChatbotAgent
from .configuration import ChatbotConfiguration
__all__ = ["ChatbotAgent"]
__all__ = ["ChatbotAgent", "ChatbotConfiguration"]

View File

@ -1,7 +1,5 @@
from dataclasses import dataclass, field
from typing import List
from langchain_core.messages import SystemMessage
from langchain_core.tools import Tool
from langchain_openai import ChatOpenAI
@ -9,20 +7,19 @@ from src import config
from src.models import select_model
from src.agents.registry import Configuration
def get_default_llm():
return select_model(config).chat_open_ai
def get_default_tools():
from langchain_community.tools.tavily_search import TavilySearchResults
return [TavilySearchResults(max_results=10)]
def get_default_requirements():
return ["TAVILY_API_KEY"]
@dataclass(kw_only=True)
class ChatbotConfiguration(Configuration):
name: str = "chatbot"
description: str = "A chatbot that can answer questions and help with tasks."
requirements: list[str] = field(default_factory=get_default_requirements)
tools: list[Tool] = field(default_factory=get_default_tools)
llm: ChatOpenAI = field(default_factory=get_default_llm)
llm: ChatOpenAI | None = None
model_provider: str = "siliconflow"
model_name: str = "Qwen/Qwen2.5-72B-Instruct"
def __post_init__(self):
# TODO 需要确保这里的模型是支持 tools 的
if self.llm is None:
self.llm = select_model(config=config,
model_provider=self.model_provider,
model_name=self.model_name).chat_open_ai

View File

@ -3,7 +3,6 @@ import uuid
from typing import Any
from datetime import datetime
from langchain_core.messages import AIMessageChunk, ToolMessage
from langchain_core.runnables import RunnableConfig
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition
@ -13,53 +12,56 @@ from src.agents.registry import State, BaseAgent
from src.agents.chatbot.configuration import ChatbotConfiguration
class ChatbotAgent(BaseAgent):
name = "chatbot"
description = "A chatbot that can answer questions and help with tasks."
_graph_cache = None
def __init__(self, configuration: ChatbotConfiguration = None):
super().__init__(configuration)
self.configuration = configuration or ChatbotConfiguration()
self.llm = configuration.llm
self.llm = self.configuration.llm
def llm_call(self, state: State) -> dict[str, Any]:
tools = self.configuration.tools
system_prompt = {
"role": "system",
"content": (
f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
)
}
messages = [system_prompt] + state["messages"]
model = self.llm.bind_tools(tools)
def _get_tools(self, config: RunnableConfig):
"""根据配置获取工具"""
tools = []
if not config:
return tools
res = model.invoke(messages)
if config.get("configurable", {}).get("use_web", None):
from langchain_community.tools.tavily_search import TavilySearchResults
tools.append(TavilySearchResults(max_results=10))
return tools
def llm_call(self, state: State, config: RunnableConfig) -> dict[str, Any]:
model = self.llm.bind_tools(self._get_tools(config))
res = model.invoke(state["messages"])
return {"messages": [res]}
def get_graph(self):
def get_graph(self, config: RunnableConfig = None):
"""构建图"""
if ChatbotAgent._graph_cache is None:
workflow = StateGraph(State)
workflow.add_node("chatbot", self.llm_call)
workflow.add_node("tools", ToolNode(tools=self.configuration.tools))
workflow.add_edge(START, "chatbot")
workflow.add_conditional_edges(
"chatbot",
tools_condition,
)
workflow.add_edge("tools", "chatbot")
workflow.add_edge("chatbot", END)
workflow = StateGraph(State)
workflow.add_node("chatbot", self.llm_call)
workflow.add_node("tools", ToolNode(tools=self._get_tools(config)))
workflow.add_edge(START, "chatbot")
workflow.add_conditional_edges(
"chatbot",
tools_condition,
)
workflow.add_edge("tools", "chatbot")
workflow.add_edge("chatbot", END)
graph = workflow.compile(checkpointer=MemorySaver())
ChatbotAgent._graph_cache = graph
return ChatbotAgent._graph_cache
graph = workflow.compile(checkpointer=MemorySaver())
return graph
def stream_values(self, messages: list[str], config: RunnableConfig = None):
graph = self.get_graph()
graph = self.get_graph(config)
for event in graph.stream({"messages": messages}, stream_mode="values", config=config):
yield event["messages"]
def stream_messages(self, messages: list[str], config: RunnableConfig = None):
graph = self.get_graph()
graph = self.get_graph(config)
for msg, metadata in graph.stream({"messages": messages}, stream_mode="messages", config=config):
msg_type = msg.type
@ -67,14 +69,13 @@ class ChatbotAgent(BaseAgent):
if not return_keys or msg_type in return_keys:
yield msg, metadata
def main():
agent = ChatbotAgent(ChatbotConfiguration())
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
from src.agents import agent_cli
from src.agents.utils import agent_cli
agent_cli(agent, config)

View File

@ -1,12 +1,11 @@
from typing import Type, Annotated, Optional, TypedDict
from abc import abstractmethod
from langchain_core.tools import BaseTool
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.base import BaseCheckpointSaver
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, ToolMessage, AIMessage, filter_messages
from langchain_core.messages import BaseMessage
from langgraph.graph.state import CompiledStateGraph
from langgraph.graph.message import add_messages
from dataclasses import dataclass
@ -26,7 +25,8 @@ class Configuration:
"""
定义一个基础 Configuration 各类 graph 继承
"""
llm: ChatOpenAI
pass
class BaseAgent():

37
src/agents/utils.py Normal file
View File

@ -0,0 +1,37 @@
from src.agents.registry import BaseAgent
from langchain_core.runnables import RunnableConfig
from langchain_core.messages import AIMessageChunk, ToolMessage
def agent_cli(agent: BaseAgent, config: RunnableConfig = None):
config = config or {}
if "configurable" not in config:
config["configurable"] = {}
while True:
user_input = input("\nUser: ")
if user_input.lower() in ["quit", "exit", "q"]:
print("Goodbye!")
break
stream_flag = False
for msg, metadata in agent.stream_messages([{"role": "user", "content": user_input}], config):
if isinstance(msg, AIMessageChunk):
content = msg.content or msg.tool_calls
if not content:
if stream_flag == True:
print()
stream_flag = False
continue
if stream_flag == False and content:
print(f"AI: {content}", end="", flush=True)
stream_flag = True
continue
elif content:
print(f"{content}", end="", flush=True)
if isinstance(msg, ToolMessage):
print(f"Tool: {msg.content}")

View File

@ -1,10 +1,14 @@
import json
import asyncio
import traceback
import uuid
from fastapi import APIRouter, Body
from fastapi.responses import StreamingResponse, Response
from src.core import HistoryManager
from fastapi.responses import StreamingResponse
from langchain_core.messages import AIMessageChunk
from src import executor, config, retriever
from src.core import HistoryManager
from src.agents import agent_manager
from src.models import select_model
from src.utils.logging_config import logger
@ -20,8 +24,8 @@ async def chat_get():
def chat_post(
query: str = Body(...),
meta: dict = Body(None),
history: list = Body(...),
cur_res_id: str = Body(...)):
history: list | None = Body(None),
thread_id: str | None = Body(None)):
model = select_model(config)
meta["server_model_name"] = model.model_name
@ -31,7 +35,6 @@ def chat_post(
def make_chunk(content=None, **kwargs):
return json.dumps({
"response": content,
"model_name": config.model_name,
"meta": meta,
**kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n"
@ -76,7 +79,7 @@ def chat_post(
else:
content += delta.content or ""
chunk = make_chunk(content=content, status="loading")
chunk = make_chunk(content=delta.content, status="loading")
yield chunk
logger.debug(f"Final response: {content}")
@ -117,4 +120,62 @@ async def call(query: str = Body(...), meta: dict = Body(None)):
response = await predict_async(query)
logger.debug({"query": query, "response": response.content})
return {"response": response.content}
return {"response": response.content}
@chat.get("/agent")
async def get_agent():
agents = [{
"name": agent["agent_class"].name,
"description": agent["agent_class"].description
} for agent in agent_manager.agents.values()]
return {"agents": agents}
@chat.post("/agent/{agent_name}")
def chat_agent(agent_name: str,
query: str = Body(...),
meta: dict = Body({}),
history: list = Body(...),
thread_id: str | None = Body(None)):
meta["server_model_name"] = agent_name
agent = agent_manager.get_runnable_agent(agent_name)
history_manager = HistoryManager(history)
messages = history_manager.get_history_with_msg(query, max_rounds=meta.get('history_round'))
history_manager.add_user(query) # 注意这里使用原始查询
runnable_config = {
"configurable": {
"thread_id": thread_id or str(uuid.uuid4()),
"use_web": meta.get("use_web", False),
"return_keys": []
}
}
def make_chunk(content=None, **kwargs):
return json.dumps({
"response": content,
"model_name": agent_name,
"meta": meta,
**kwargs
}, ensure_ascii=False).encode('utf-8') + b"\n"
def stream_messages():
content = ""
yield make_chunk(status="waiting")
for msg, metadata in agent.stream_messages(messages, runnable_config):
# logger.debug(f"msg: {msg.model_dump()}, {metadata=}")
if isinstance(msg, AIMessageChunk) and msg.content != "<tool_call>":
content += msg.content
yield make_chunk(content=msg.content,
msg=msg.model_dump(),
metadata=metadata,
status="loading")
else:
yield make_chunk(msg=msg.model_dump(),
metadata=metadata,
status="loading")
yield make_chunk(status="finished", history=history_manager.update_ai(content))
return StreamingResponse(stream_messages(), media_type='application/json')

View File

@ -370,7 +370,7 @@ const updateMessage = (info) => {
try {
// text
if (info.text !== null && info.text !== undefined && info.text !== '') {
msg.text = info.text;
msg.text += info.text;
}
if (info.reasoning_content !== null && info.reasoning_content !== undefined && info.reasoning_content !== '') {
@ -463,6 +463,7 @@ const fetchChatResponse = (user_input, cur_res_id) => {
history: conv.value.history,
meta: meta,
cur_res_id: cur_res_id,
thread_id: conv.value.id.toString(),
}),
headers: {
'Content-Type': 'application/json'
@ -639,6 +640,12 @@ watch(
display: flex;
align-items: center;
}
.header__left {
.close {
margin-right: 12px;
}
}
}
.nav-btn {
@ -652,6 +659,7 @@ watch(
font-size: 1rem;
width: auto;
padding: 0.5rem 1rem;
transition: background-color 0.3s;
.text {
margin-left: 10px;
@ -678,6 +686,7 @@ watch(
padding: 12px;
z-index: 11;
width: 280px;
transition: transform 0.3s ease, opacity 0.3s ease;
.flex-center {
display: flex;
@ -1051,9 +1060,27 @@ watch(
@keyframes loading {0%,80%,100%{transform:scale(0.5);}40%{transform:scale(1);}}
.slide-out-left{-webkit-animation:slide-out-left .2s cubic-bezier(.55,.085,.68,.53) both;animation:slide-out-left .5s cubic-bezier(.55,.085,.68,.53) both}
.swing-in-top-fwd{-webkit-animation:swing-in-top-fwd .2s ease-out both;animation:swing-in-top-fwd .2s ease-out both}
@-webkit-keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}@keyframes swing-in-top-fwd{0%{-webkit-transform:rotateX(-100deg);transform:rotateX(-100deg);-webkit-transform-origin:top;transform-origin:top;opacity:0}100%{-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transform-origin:top;transform-origin:top;opacity:1}}
@-webkit-keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}@keyframes slide-out-left{0%{-webkit-transform:translateX(0);transform:translateX(0);opacity:1}100%{-webkit-transform:translateX(-1000px);transform:translateX(-1000px);opacity:0}}
.swing-in-top-fwd {
-webkit-animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
animation: swing-in-top-fwd 0.3s cubic-bezier(0.175, 0.885, 0.320, 1.275) both;
}
@keyframes swing-in-top-fwd {
0% {
-webkit-transform: rotateX(-100deg);
transform: rotateX(-100deg);
-webkit-transform-origin: top;
transform-origin: top;
opacity: 0;
}
100% {
-webkit-transform: rotateX(0deg);
transform: rotateX(0deg);
-webkit-transform-origin: top;
transform-origin: top;
opacity: 1;
}
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideInUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }

View File

@ -21,6 +21,8 @@ import {
StarFilled,
StarOutlined,
ExclamationCircleOutlined,
RobotOutlined,
RobotFilled,
} from '@ant-design/icons-vue'
import { themeConfig } from '@/assets/theme'
import { useConfigStore } from '@/stores/config'
@ -73,6 +75,37 @@ onMounted(() => {
// 使 vue3 setup composition API
const route = useRoute()
console.log(route)
//
const mainList = [{
name: '对话',
path: '/chat',
icon: MessageOutlined,
activeIcon: MessageFilled,
}, {
name: '智能体',
path: '/agent',
icon: RobotOutlined,
activeIcon: RobotFilled,
}, {
name: '图谱',
path: '/graph',
icon: ProjectOutlined,
activeIcon: ProjectFilled,
// hidden: !configStore.config.enable_knowledge_graph,
}, {
name: '知识库',
path: '/database',
icon: BookOutlined,
activeIcon: BookFilled,
// hidden: !configStore.config.enable_knowledge_base,
}, {
name: '工具',
path: '/tools',
icon: ToolOutlined,
activeIcon: ToolFilled,
}
]
</script>
<template>
@ -107,22 +140,18 @@ console.log(route)
</router-link>
</div>
<div class="nav">
<RouterLink to="/chat" class="nav-item" active-class="active">
<component class="icon" :is="route.path === '/chat' ? MessageFilled : MessageOutlined" />
<span class="text">对话</span>
</RouterLink>
<RouterLink to="/database" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/database') ? FolderFilled : FolderOutlined" />
<span class="text">知识</span>
</RouterLink>
<RouterLink to="/graph" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/graph') ? ProjectFilled: ProjectOutlined" />
<span class="text">图谱</span>
</RouterLink>
<RouterLink to="/tools" class="nav-item" active-class="active">
<component class="icon" :is="route.path.startsWith('/tools') ? ToolFilled: ToolOutlined" />
<span class="text">工具</span>
<!-- 使用mainList渲染导航项 -->
<RouterLink
v-for="(item, index) in mainList"
:key="index"
:to="item.path"
v-show="!item.hidden"
class="nav-item"
active-class="active">
<component class="icon" :is="route.path.startsWith(item.path) ? item.activeIcon : item.icon" />
<span class="text">{{item.name}}</span>
</RouterLink>
<a-tooltip placement="right">
<template #title>后端疑似没有正常启动或者正在繁忙中请刷新一下或者检查 docker logs api-dev</template>
<div class="nav-item warning" v-if="!configStore.config._config_items">
@ -140,7 +169,7 @@ console.log(route)
</span>
</a>
</div>
<RouterLink class="nav-item setting" to="/setting" active-class="active">
<RouterLink class="nav-item setting" to="/setting" active-class="active">
<component class="icon" :is="route.path === '/setting' ? SettingFilled : SettingOutlined" />
</RouterLink>
</div>

View File

@ -30,6 +30,19 @@ const router = createRouter({
}
]
},
{
path: '/agent',
name: 'agent',
component: AppLayout,
children: [
{
path: '',
name: 'AgentComp',
component: () => import('../views/AgentView.vue'),
meta: { keepAlive: true }
}
]
},
{
path: '/graph',
name: 'graph',

1694
web/src/views/AgentView.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
<template>
<div class="chat-container">
<div class="conversations" :class="['conversations', { 'is-open': state.isSidebarOpen }]">
<div class="conversations" :class="{ 'is-open': state.isSidebarOpen }">
<div class="actions">
<!-- <div class="action new" @click="addNewConv"><FormOutlined /></div> -->
<span class="header-title">对话历史</span>
@ -129,29 +129,24 @@ onMounted(() => {
position: relative;
}
.chat-container .conversations {
transition: width 0.3s ease, opacity 0.3s ease, flex 0.3s ease;
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 确保内容不溢出 */
}
.chat-container .conversations:not(.is-open) {
width: 0;
opacity: 0;
flex: 0 0 0;
}
.chat-container .conversations.is-open {
flex: 1 1 auto; /* 当侧边栏打开时,占据可用空间 */
width: 230px;
opacity: 1;
}
.conversations {
width: 230px; /* 初始宽度 */
width: 230px;
max-width: 230px;
border-right: 1px solid var(--main-light-3);
max-height: 100%;
background-color: var(--bg-sider);
transition: all 0.3s ease;
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 确保内容不溢出 */
&.is-open {
width: 230px;
}
&:not(.is-open) {
width: 0;
padding: 0;
overflow: hidden;
}
& .actions {
height: var(--header-height);
@ -238,7 +233,6 @@ onMounted(() => {
display: block;
}
}
}
}
@ -275,6 +269,12 @@ onMounted(() => {
height: 100%;
border-radius: 0 16px 16px 0;
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05);
&:not(.is-open) {
width: 0;
padding: 0;
overflow: hidden;
}
}
}
</style>