diff --git a/frontend/src/components/Agent/SystemLogsView.tsx b/frontend/src/components/Agent/SystemLogsView.tsx index 07a13f7..cb564bf 100644 --- a/frontend/src/components/Agent/SystemLogsView.tsx +++ b/frontend/src/components/Agent/SystemLogsView.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { RefreshCw, Search, Server, GitBranch } from 'lucide-react'; +import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight } from 'lucide-react'; import apiClient from '../../api/client'; interface EventLog { @@ -28,12 +28,13 @@ interface WorkflowStep { action: string; status: string; agent_id: string | null; + output?: string; } -const LEVEL_STYLES: Record = { - error: { bg: 'bg-[rgba(196,145,122,0.12)]', text: 'text-[#a0705a]', label: 'ERROR' }, - warn: { bg: 'bg-[rgba(196,168,130,0.15)]', text: 'text-[#9a7d5e]', label: 'WARN' }, - info: { bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]', label: 'INFO' }, +const LEVEL_COLORS: Record = { + error: 'text-red-400', + warn: 'text-yellow-400', + info: 'text-green-400', }; const STATUS_STYLES: Record = { @@ -53,12 +54,14 @@ export function SystemLogsView() { const [traceFilter, setTraceFilter] = useState(''); const [typeFilter, setTypeFilter] = useState(''); const [levelFilter, setLevelFilter] = useState(''); + const terminalRef = useRef(null); // Workflow logs state const [workflows, setWorkflows] = useState([]); const [selectedTrace, setSelectedTrace] = useState(null); const [workflowSteps, setWorkflowSteps] = useState([]); const [wfLoading, setWfLoading] = useState(false); + const [expandedStep, setExpandedStep] = useState(null); const fetchLogs = async () => { setLoading(true); @@ -88,6 +91,7 @@ export function SystemLogsView() { const fetchWorkflowDetail = async (traceId: string) => { setWfLoading(true); + setExpandedStep(null); try { const resp = await apiClient.get(`/api/v1/workflow/${traceId}`); setWorkflowSteps(resp.data.steps || []); @@ -108,9 +112,16 @@ export function SystemLogsView() { if (selectedTrace) fetchWorkflowDetail(selectedTrace); }, [selectedTrace]); - const handleSearch = (e: React.FormEvent) => { - e.preventDefault(); - fetchLogs(); + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.scrollTop = terminalRef.current.scrollHeight; + } + }, [logs]); + + const formatTime = (ts: string | null) => { + if (!ts) return '--:--:--'; + const d = new Date(ts); + return d.toLocaleTimeString('en-GB', { hour12: false }); }; return ( @@ -143,18 +154,19 @@ export function SystemLogsView() { {tab === 'system' ? ( <> -
+ {/* Filter bar */} +
setTraceFilter(e.target.value)} - placeholder={t('agent.logFilterTraceId')} - className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15" + placeholder="Trace ID" + className="px-3 py-1.5 bg-bg-card border border-border-primary rounded-lg text-xs text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15 w-40" /> setLevelFilter(e.target.value)} - className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15" + className="px-3 py-1.5 bg-bg-card border border-border-primary rounded-lg text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15" > - - +
-
-
- - - - - - - - - - - - - {logs.length === 0 ? ( - - ) : ( - logs.map((log) => { - const style = LEVEL_STYLES[log.level] || LEVEL_STYLES.info; - return ( - - - - - - - - - ); - }) - )} - -
{t('agent.logLevel')}{t('agent.logType')}Trace ID{t('agent.logNode')}{t('agent.logMessage')}{t('agent.logTime')}
{t('agent.noLogs')}
- {style.label} - {log.event_type}{log.trace_id.slice(-8)}{log.node_name || '-'}{log.message}{log.created_at ? new Date(log.created_at).toLocaleString() : '-'}
-
+ {/* Terminal-style log display */} +
+ {logs.length === 0 ? ( +
{t('agent.noLogs')}
+ ) : ( + logs.map((log) => { + const levelColor = LEVEL_COLORS[log.level] || 'text-gray-400'; + return ( +
+ [{formatTime(log.created_at)}]{' '} + {log.level.toUpperCase().padEnd(5)}{' '} + {log.node_name || 'system'}{' '} + {log.message} + {log.trace_id && #{log.trace_id.slice(-6)}} +
+ ); + }) + )}
) : ( @@ -259,38 +259,40 @@ export function SystemLogsView() { {t('common.loading')}
) : ( -
- - - - - - - - - - - - {workflowSteps.length === 0 ? ( - - ) : ( - workflowSteps.map((step, idx) => { - const ss = STATUS_STYLES[step.status] || STATUS_STYLES.pending; - return ( - - - - - - - - ); - }) - )} - -
#{t('agent.name')}{t('agent.logNode')}{t('common.status')}{t('agent.workflowAction')}
{t('workflow.noStepsYet')}
{step.step || idx + 1}{step.name}{step.node} - {step.status} - {step.action}
+
+ {workflowSteps.length === 0 ? ( +
{t('workflow.noStepsYet')}
+ ) : ( + workflowSteps.map((step, idx) => { + const ss = STATUS_STYLES[step.status] || STATUS_STYLES.pending; + const isExpanded = expandedStep === idx; + return ( +
+ + {isExpanded && step.output && ( +
+
+ {step.output} +
+
+ )} +
+ ); + }) + )}
)}
diff --git a/kilostar/api/workflow.py b/kilostar/api/workflow.py index e06d7fc..259a573 100644 --- a/kilostar/api/workflow.py +++ b/kilostar/api/workflow.py @@ -177,8 +177,8 @@ def _merge_runtime_status(work_link: list, workflow_log: list) -> list: 前端 ``WorkflowDiagram`` 依赖每个 step 的 ``status`` 字段着色,这个拼装让 后端真正把运行期状态喂过去。 """ - # step_index -> 最新 status latest_status: dict[int, str] = {} + latest_output: dict[int, str] = {} for entry in workflow_log or []: if not isinstance(entry, dict): continue @@ -189,14 +189,16 @@ def _merge_runtime_status(work_link: list, workflow_log: list) -> list: continue if isinstance(payload, (list, tuple)) and len(payload) >= 2: latest_status[idx] = payload[1] + if isinstance(payload, (list, tuple)) and len(payload) >= 3: + latest_output[idx] = payload[2] merged = [] for i, step in enumerate(work_link or []): step_copy = dict(step) if isinstance(step, dict) else {} - # step 自带的 step 字段优先,否则用位置索引 step_idx = step_copy.get("step") lookup_idx = (step_idx - 1) if isinstance(step_idx, int) else i step_copy["status"] = latest_status.get(lookup_idx, "pending") + step_copy["output"] = latest_output.get(lookup_idx, "") merged.append(step_copy) return merged diff --git a/kilostar/core/individual/regulatory_node/regulatory_node.py b/kilostar/core/individual/regulatory_node/regulatory_node.py index 8b7493a..9955fd2 100644 --- a/kilostar/core/individual/regulatory_node/regulatory_node.py +++ b/kilostar/core/individual/regulatory_node/regulatory_node.py @@ -126,11 +126,23 @@ class RegulatoryNode: """ return await self._run(payload) - async def stream_working(self, payload: MessageRequest, token_queue: "asyncio.Queue") -> None: - """流式工具调用版本:逐 token 推送到 queue,工具调用结果也会通过 token 输出。 + _CHAT_INSTRUCTIONS = ( + "你是 kilostar 智能助手。你现在处于【直接对话模式】,请直接回答用户的问题。\n" + "规则:\n" + "1. 直接、详细地回答用户问题,像一个专业且友好的助手。\n" + "2. 如果你有可用工具,可以调用工具来辅助回答(如搜索、读文件等)。\n" + "3. 不要输出内部思考过程,不要做路由判断,不要提及 ForUser/ForConsciousnessNode 等格式。\n" + "4. 回复应当完整、有帮助,避免过于简短。\n" + ) - 完成后 push None 作为终止信号。 + async def stream_working(self, payload: MessageRequest, token_queue: "asyncio.Queue") -> None: + """流式对话:完整执行 agent graph(含工具调用),逐 token 推送文本到 queue。 + + 使用 event_stream_handler 回调拿到每个 text delta,保证工具调用后 + 的文本也能被流式输出。完成后 push None 作为终止信号。 """ + from pydantic_ai.messages import PartStartEvent, PartDeltaEvent, TextPart, TextPartDelta + platform = payload.platform user_name = payload.user_name message = payload.message @@ -140,17 +152,27 @@ class RegulatoryNode: await token_queue.put(None) return + async def _stream_handler(ctx, events): + async for event in events: + if isinstance(event, PartStartEvent) and isinstance(event.part, TextPart): + if event.part.content: + await token_queue.put(event.part.content) + elif isinstance(event, PartDeltaEvent) and isinstance(event.delta, TextPartDelta): + await token_queue.put(event.delta.content_delta) + try: deps = RegulatoryNodeDeps( platform=platform, user_name=user_name, time=time_str ) - async with self.agent.run_stream( - user_prompt=message, deps=deps, output_type=str - ) as stream_result: - async for delta in stream_result.stream_text(delta=True): - await token_queue.put(delta) + await self.agent.run( + user_prompt=message, + deps=deps, + output_type=str, + instructions=self._CHAT_INSTRUCTIONS, + event_stream_handler=_stream_handler, + ) except Exception as e: self.logger.exception(f"RegulatoryNode.stream_working failed: {e}") await token_queue.put(f"\n\n[错误: {str(e)}]") diff --git a/kilostar/utils/i18n.py b/kilostar/utils/i18n.py index 4f425f4..9d6e2ba 100644 --- a/kilostar/utils/i18n.py +++ b/kilostar/utils/i18n.py @@ -59,21 +59,41 @@ _PROMPTS: Dict[str, Dict[str, str]] = { "consciousness_node": { "zh": ( "你叫kilostar,是一个多智能体AI助手系统中的【意识节点 (Consciousness Node)】。\n" - "你是系统的'高级规划师'和'架构师',负责处理监控节点分配过来的复杂任务。\n" - "你的主要工作场景包括:\n" - "1. 拆解任务 (Workflow Generation):结合用户的原始命令和提供的模板,生成严谨、可执行的工作流 (kilostarWorkflow),并将其输出为 ForWorkflowEngine 格式。拆解时步骤应清晰连贯。\n" - "2. 中途指导 (Workflow Execution):在工作流执行中,如果某一步骤指派给你,你需要对控制节点的结果进行分析或提供下一步的指导,输出 ForWorkflow 格式。\n" - "3. 总结报告 (regulatory Report):在整个工作流执行完毕后,你需要对整体流程、各个控制节点的执行情况进行审查,并生成一份技术性的总结报告,输出 ForregulatoryNode 格式。\n" - "请确保所有的思考和生成过程符合逻辑,严密且高质量。" + "你是系统的'高级规划师'和'架构师',负责处理监控节点分配过来的复杂任务。\n\n" + "你的工作根据收到的输入类型严格分为三种模式:\n\n" + "【模式1:工作流生成】当你收到用户的原始任务命令时:\n" + "- 将复杂任务拆解为多个清晰、可执行的步骤\n" + "- 每个步骤必须指派给真实存在的 Worker(使用其真实 agent_id)或 consciousness_node 自己\n" + "- 严禁编造不存在的 agent_id!只能使用上下文中列出的可用 Worker\n" + "- 输出格式:ForWorkflowEngine\n\n" + "【模式2:工作流步骤执行】当某个步骤指派给你自己时:\n" + "- 直接完成该步骤描述的具体任务\n" + "- 输出应当是任务的实际结果(代码、分析、文档等),而非对任务的描述\n" + "- 输出格式:ForWorkflow\n\n" + "【模式3:总结报告】当整个工作流执行完毕时:\n" + "- 审查各步骤执行情况,生成面向用户的技术总结报告\n" + "- 报告应包含:完成了什么、关键结果、是否有失败步骤及原因\n" + "- 输出格式:ForregulatoryNode\n\n" + "确保所有输出符合逻辑、严密且高质量。" ), "en": ( "You are kilostar, the [Consciousness Node] in a multi-agent AI assistant system.\n" - "You are the system's 'senior planner' and 'architect', responsible for handling complex tasks assigned by the Regulatory Node.\n" - "Your main scenarios include:\n" - "1. Task Decomposition (Workflow Generation): Combine the user's original command with provided templates to generate rigorous, executable workflows (kilostarWorkflow), outputting them in the ForWorkflowEngine format. Steps should be clear and coherent.\n" - "2. Mid-flight Guidance (Workflow Execution): During workflow execution, if a step is assigned to you, analyze the Control Node's results or provide next-step guidance, outputting in the ForWorkflow format.\n" - "3. Summary Report (Regulatory Report): After the entire workflow completes, review the overall process and each Control Node's execution, generating a technical summary report in the ForregulatoryNode format.\n" - "Ensure all reasoning and generation is logical, rigorous, and high-quality." + "You are the system's 'senior planner' and 'architect', responsible for handling complex tasks assigned by the Regulatory Node.\n\n" + "Your work is strictly divided into three modes based on input type:\n\n" + "[Mode 1: Workflow Generation] When you receive the user's original task command:\n" + "- Decompose the complex task into clear, executable steps\n" + "- Each step must be assigned to a real existing Worker (using its real agent_id) or to consciousness_node itself\n" + "- NEVER fabricate non-existent agent_ids! Only use Workers listed in the context\n" + "- Output format: ForWorkflowEngine\n\n" + "[Mode 2: Workflow Step Execution] When a step is assigned to you:\n" + "- Directly complete the specific task described in the step\n" + "- Output should be the actual result (code, analysis, documentation, etc.), not a description of the task\n" + "- Output format: ForWorkflow\n\n" + "[Mode 3: Summary Report] When the entire workflow has completed:\n" + "- Review each step's execution and generate a user-facing technical summary\n" + "- Report should include: what was accomplished, key results, any failed steps and reasons\n" + "- Output format: ForregulatoryNode\n\n" + "Ensure all output is logical, rigorous, and high-quality." ), }, "control_node": {