Skip to content

Commit 0d20cf6

Browse files
committed
feat(agent): 添加智能体工作状态显示功能
实现智能体工作状态的实时显示,包括任务列表和文件管理功能 - 在 DeepAgent 中添加 todo 和 files 能力支持 - 新增 AgentPopover 组件展示工作状态 - 修改聊天界面添加状态显示按钮 - 更新服务端状态提取逻辑 - 更新文档和路线图
1 parent 15acdaa commit 0d20cf6

File tree

6 files changed

+628
-14
lines changed

6 files changed

+628
-14
lines changed

docs/latest/changelog/roadmap.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
### 看板
88

9-
- 新建 DeepAgents 智能体(暂时没有场景)
109
- 统一图谱数据结构,优化可视化方式 [#298](https://github.com/xerrors/Yuxi-Know/issues/298) [#273](https://github.com/xerrors/Yuxi-Know/issues/273) <Badge type="info" text="0.4" />
1110
- 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示
1211
- 开发与生产环境隔离,构建生产镜像 <Badge type="info" text="0.4" />
@@ -17,12 +16,14 @@
1716
### Bugs
1817
- 部分异常状态下,智能体的模型名称出现重叠[#279](https://github.com/xerrors/Yuxi-Know/issues/279)
1918
- DeepSeek 官方接口适配会出现问题
19+
- 当前版本如果调用结果为空的时候,工具调用状态会一直处于调用状态,尽管调用是成功的
2020

2121
### 新增
2222
- 优化知识库详情页面,更加简洁清晰
2323
- 新增对于上传文件的智能体中间件
2424
- 增强文件下载功能
2525
- 新增多模态模型支持(当前仅支持图片,详见文档)
26+
- 新建 DeepAgents 智能体(Demo)
2627

2728
### 修复
2829
- 修复重排序模型实际未生效的问题

server/routers/chat_router.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ async def set_default_agent(request_data: dict = Body(...), current_user=Depends
102102

103103

104104
async def _get_langgraph_messages(agent_instance, config_dict):
105-
"""获取LangGraph中的消息"""
106105
graph = await agent_instance.get_graph()
107106
state = await graph.aget_state(config_dict)
108107

@@ -113,6 +112,24 @@ async def _get_langgraph_messages(agent_instance, config_dict):
113112
return state.values.get("messages", [])
114113

115114

115+
def _extract_agent_state(values: dict) -> dict:
116+
if not isinstance(values, dict):
117+
return {}
118+
119+
def _norm_list(v):
120+
if v is None:
121+
return []
122+
if isinstance(v, (list, tuple)):
123+
return list(v)
124+
return [v]
125+
126+
result = {}
127+
result["todos"] = _norm_list(values.get("todos"))[:20]
128+
result["files"] = _norm_list(values.get("files"))[:50]
129+
130+
return result
131+
132+
116133
def _get_existing_message_ids(conv_mgr, thread_id):
117134
"""获取已保存的消息ID集合"""
118135
existing_messages = conv_mgr.get_messages_by_thread_id(thread_id)
@@ -521,6 +538,7 @@ async def stream_messages():
521538

522539
try:
523540
full_msg = None
541+
langgraph_config = {"configurable": input_context}
524542
async for msg, metadata in agent.stream_messages(messages, input_context=input_context):
525543
if isinstance(msg, AIMessageChunk):
526544
full_msg = msg if not full_msg else full_msg + msg
@@ -534,7 +552,18 @@ async def stream_messages():
534552
yield make_chunk(content=msg.content, msg=msg.model_dump(), metadata=metadata, status="loading")
535553

536554
else:
537-
yield make_chunk(msg=msg.model_dump(), metadata=metadata, status="loading")
555+
msg_dict = msg.model_dump()
556+
yield make_chunk(msg=msg_dict, metadata=metadata, status="loading")
557+
558+
try:
559+
if msg_dict.get("type") == "tool":
560+
graph = await agent.get_graph()
561+
state = await graph.aget_state(langgraph_config)
562+
agent_state = _extract_agent_state(getattr(state, "values", {})) if state else {}
563+
if agent_state:
564+
yield make_chunk(status="agent_state", agent_state=agent_state, meta=meta)
565+
except Exception:
566+
pass
538567

539568
if (
540569
conf.enable_content_guard
@@ -548,13 +577,22 @@ async def stream_messages():
548577
return
549578

550579
# After streaming finished, check for interrupts and save messages
551-
langgraph_config = {"configurable": input_context}
552580

553581
# Check for human approval interrupts
554582
async for chunk in check_and_handle_interrupts(agent, langgraph_config, make_chunk, meta, thread_id):
555583
yield chunk
556584

557585
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
586+
try:
587+
graph = await agent.get_graph()
588+
state = await graph.aget_state(langgraph_config)
589+
agent_state = _extract_agent_state(getattr(state, "values", {})) if state else {}
590+
except Exception:
591+
agent_state = {}
592+
593+
if agent_state:
594+
yield make_chunk(status="agent_state", agent_state=agent_state, meta=meta)
595+
558596
yield make_chunk(status="finished", meta=meta)
559597

560598
# Save all messages from LangGraph state

src/agents/deep_agent/graph.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ class DeepAgent(BaseAgent):
179179
context_schema = DeepContext
180180
capabilities = [
181181
"file_upload",
182+
"todo",
183+
"files",
182184
]
183185

184186
def __init__(self, **kwargs):

web/src/components/AgentChatComponent.vue

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@
4343
</div>
4444
</div>
4545
<div class="header__right">
46+
<!-- AgentState 显示按钮 - 只在智能体支持 todo 或 files 能力时显示 -->
47+
<AgentPopover
48+
v-model:visible="agentStatePopoverVisible"
49+
:agent-state="currentAgentState"
50+
>
51+
<div
52+
class="agent-nav-btn agent-state-btn"
53+
:class="{ 'has-content': hasAgentStateContent }"
54+
:title="hasAgentStateContent ? '查看工作状态' : '暂无工作状态'"
55+
>
56+
<Activity class="nav-btn-icon" size="18"/>
57+
<span v-if="hasAgentStateContent" class="text">{{ totalAgentStateItems }}</span>
58+
</div>
59+
</AgentPopover>
4660
<!-- <div class="nav-btn" @click="shareChat" v-if="currentChatId && currentAgent">
4761
<ShareAltOutlined style="font-size: 18px;"/>
4862
</div> -->
@@ -202,7 +216,8 @@
202216
</div>
203217
</div>
204218
</div>
205-
</div>
219+
220+
</div>
206221
</template>
207222

208223
<script setup>
@@ -217,7 +232,7 @@ import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
217232
import ImagePreviewComponent from '@/components/ImagePreviewComponent.vue'
218233
import ChatSidebarComponent from '@/components/ChatSidebarComponent.vue'
219234
import RefsComponent from '@/components/RefsComponent.vue'
220-
import { PanelLeftOpen, MessageCirclePlus, LoaderCircle } from 'lucide-vue-next';
235+
import { PanelLeftOpen, MessageCirclePlus, LoaderCircle, Activity } from 'lucide-vue-next';
221236
import { handleChatError, handleValidationError } from '@/utils/errorHandler';
222237
import { ScrollController } from '@/utils/scrollController';
223238
import { AgentValidator } from '@/utils/agentValidator';
@@ -228,6 +243,7 @@ import { MessageProcessor } from '@/utils/messageProcessor';
228243
import { agentApi, threadApi } from '@/apis';
229244
import HumanApprovalModal from '@/components/HumanApprovalModal.vue';
230245
import { useApproval } from '@/composables/useApproval';
246+
import AgentPopover from '@/components/AgentPopover.vue';
231247
232248
// ==================== PROPS & EMITS ====================
233249
const props = defineProps({
@@ -293,6 +309,9 @@ const attachmentState = reactive({
293309
isUploading: false,
294310
})
295311
312+
// AgentState Popover 状态
313+
const agentStatePopoverVisible = ref(false);
314+
296315
// ==================== COMPUTED PROPERTIES ====================
297316
const currentAgentId = computed(() => {
298317
if (props.singleMode) {
@@ -333,6 +352,37 @@ const supportsFileUpload = computed(() => {
333352
const capabilities = currentAgent.value.capabilities || [];
334353
return capabilities.includes('file_upload');
335354
});
355+
const supportsTodo = computed(() => {
356+
if (!currentAgent.value) return false;
357+
const capabilities = currentAgent.value.capabilities || [];
358+
return capabilities.includes('todo');
359+
});
360+
361+
const supportsFiles = computed(() => {
362+
if (!currentAgent.value) return false;
363+
const capabilities = currentAgent.value.capabilities || [];
364+
return capabilities.includes('files');
365+
});
366+
367+
// AgentState 相关计算属性
368+
const currentAgentState = computed(() => {
369+
return currentChatId.value ? getThreadState(currentChatId.value)?.agentState || null : null;
370+
});
371+
372+
const hasAgentStateContent = computed(() => {
373+
const agentState = currentAgentState.value;
374+
if (!agentState) return false;
375+
return (agentState.todos && agentState.todos.length > 0) ||
376+
(agentState.files && Object.keys(agentState.files).length > 0);
377+
});
378+
379+
const totalAgentStateItems = computed(() => {
380+
const agentState = currentAgentState.value;
381+
if (!agentState) return 0;
382+
const todoCount = agentState.todos ? agentState.todos.length : 0;
383+
const fileCount = agentState.files ? Object.keys(agentState.files).length : 0;
384+
return todoCount + fileCount;
385+
});
336386
337387
const currentThreadMessages = computed(() => threadMessages.value[currentChatId.value] || []);
338388
@@ -427,7 +477,7 @@ onMounted(() => {
427477
onUnmounted(() => {
428478
if (resizeObserver) resizeObserver.disconnect();
429479
scrollController.cleanup();
430-
// 清理所有线程状态
480+
// 清理所有线程状态
431481
resetOnGoingConv();
432482
});
433483
@@ -439,7 +489,8 @@ const getThreadState = (threadId) => {
439489
chatState.threadStates[threadId] = {
440490
isStreaming: false,
441491
streamAbortController: null,
442-
onGoingConv: createOnGoingConvState()
492+
onGoingConv: createOnGoingConvState(),
493+
agentState: null // 添加 agentState 字段
443494
};
444495
}
445496
return chatState.threadStates[threadId];
@@ -548,10 +599,27 @@ const _processStreamChunk = (chunk, threadId) => {
548599
case 'human_approval_required':
549600
// 使用审批 composable 处理审批请求
550601
return processApprovalInStream(chunk, threadId, currentAgentId.value);
602+
case 'agent_state':
603+
if ((supportsTodo.value || supportsFiles.value) && chunk.agent_state) {
604+
console.log('[AgentState]', {
605+
threadId,
606+
todos: chunk.agent_state?.todos || [],
607+
files: chunk.agent_state?.files || []
608+
});
609+
threadState.agentState = chunk.agent_state;
610+
}
611+
return false;
551612
case 'finished':
552613
// 先标记流式结束,但保持消息显示直到历史记录加载完成
553614
if (threadState) {
554615
threadState.isStreaming = false;
616+
if ((supportsTodo.value || supportsFiles.value) && threadState.agentState) {
617+
console.log('[AgentState|Final]', {
618+
threadId,
619+
todos: threadState.agentState?.todos || [],
620+
files: threadState.agentState?.files || []
621+
});
622+
}
555623
}
556624
// 异步加载历史记录,保持当前消息显示直到历史记录加载完成
557625
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId })
@@ -1231,6 +1299,7 @@ const loadChatsList = async () => {
12311299
}
12321300
};
12331301
1302+
12341303
const initAll = async () => {
12351304
try {
12361305
if (!agentStore.isInitialized) {
@@ -1244,7 +1313,7 @@ const initAll = async () => {
12441313
onMounted(async () => {
12451314
await initAll();
12461315
scrollController.enableAutoScroll();
1247-
});
1316+
});
12481317
12491318
watch(currentAgentId, async (newAgentId, oldAgentId) => {
12501319
if (newAgentId !== oldAgentId) {
@@ -1769,6 +1838,16 @@ watch(conversations, () => {
17691838
}
17701839
}
17711840
1841+
/* AgentState 按钮有内容时的样式 */
1842+
.agent-nav-btn.agent-state-btn.has-content {
1843+
color: var(--main-600);
1844+
1845+
&:hover:not(.is-disabled) {
1846+
color: var(--main-700);
1847+
background-color: var(--main-40);
1848+
}
1849+
}
1850+
17721851
@keyframes spin {
17731852
from {
17741853
transform: rotate(0deg);

web/src/components/AgentMessageComponent.vue

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,6 @@ const validToolCalls = computed(() => {
194194
});
195195
196196
const parsedData = computed(() => {
197-
// 调试工具调用处理
198-
if (validToolCalls.value && validToolCalls.value.length > 0) {
199-
console.log('Valid tool calls in message:', validToolCalls.value);
200-
}
201-
202197
// Start with default values from the prop to avoid mutation.
203198
let content = props.message.content.trim() || '';
204199
let reasoning_content = props.message.additional_kwargs?.reasoning_content || '';

0 commit comments

Comments
 (0)