myagent-ai 1.6.5 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agents/base.py CHANGED
@@ -145,6 +145,7 @@ class BaseAgent(ABC):
145
145
  request_kwargs.update(kwargs)
146
146
 
147
147
  full_text = ""
148
+ full_reasoning = "" # Track reasoning tokens (for o1/o3/DeepSeek-R1 etc.)
148
149
  tool_calls_acc: Dict[int, Dict] = {} # index -> {id, name, arguments_str}
149
150
  finish_reason = ""
150
151
 
@@ -166,6 +167,12 @@ class BaseAgent(ABC):
166
167
  else:
167
168
  await _write_sse({"type": "text_delta", "content": delta_text})
168
169
 
170
+ async def _emit_reasoning_delta(delta_text: str):
171
+ """处理推理 token:发送 reasoning_delta SSE 事件"""
172
+ nonlocal full_reasoning
173
+ full_reasoning += delta_text
174
+ await _write_sse({"type": "reasoning_delta", "content": delta_text})
175
+
169
176
  try:
170
177
  if self.llm.provider in self.llm._OPENAI_COMPATIBLE_PROVIDERS or self.llm.provider == "zhipu":
171
178
  # 使用异步客户端流式
@@ -190,6 +197,11 @@ class BaseAgent(ABC):
190
197
  if delta.content:
191
198
  await _emit_text_delta(delta.content)
192
199
 
200
+ # Handle reasoning_content (OpenAI o1/o3, DeepSeek-R1, Qwen-QwQ etc.)
201
+ reasoning_content = getattr(delta, 'reasoning_content', None) or getattr(delta, 'reasoning', None)
202
+ if reasoning_content:
203
+ await _emit_reasoning_delta(reasoning_content)
204
+
193
205
  # Handle tool_call deltas (accumulate)
194
206
  if hasattr(delta, 'tool_calls') and delta.tool_calls:
195
207
  for tc_delta in delta.tool_calls:
@@ -252,8 +264,17 @@ class BaseAgent(ABC):
252
264
  if event is None:
253
265
  break
254
266
  if event.type == "content_block_delta":
255
- if hasattr(event.delta, "text"):
267
+ # Handle text content
268
+ if hasattr(event.delta, "text") and event.delta.text:
256
269
  await _emit_text_delta(event.delta.text)
270
+ # Handle extended thinking (Anthropic reasoning)
271
+ if hasattr(event.delta, "thinking") and event.delta.thinking:
272
+ await _emit_reasoning_delta(event.delta.thinking)
273
+ # Handle thinking delta via type
274
+ if hasattr(event.delta, "type") and event.delta.type == "thinking_delta":
275
+ thinking_text = getattr(event.delta, "thinking", "")
276
+ if thinking_text:
277
+ await _emit_reasoning_delta(thinking_text)
257
278
  elif event.type == "message_stop":
258
279
  finish_reason = "stop"
259
280
 
@@ -292,9 +313,14 @@ class BaseAgent(ABC):
292
313
  break
293
314
  try:
294
315
  data = json.loads(line.decode('utf-8') if isinstance(line, bytes) else line)
295
- content = data.get("message", {}).get("content", "")
316
+ message = data.get("message", {})
317
+ content = message.get("content", "")
296
318
  if content:
297
319
  await _emit_text_delta(content)
320
+ # Handle Ollama thinking/reasoning (e.g., DeepSeek-R1 via Ollama)
321
+ thinking = message.get("thinking", "")
322
+ if thinking:
323
+ await _emit_reasoning_delta(thinking)
298
324
  if data.get("done"):
299
325
  finish_reason = "stop"
300
326
  # Record usage from Ollama
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -592,7 +592,16 @@ class ApiServer:
592
592
  agent_path=agent_path, agent_system_prompt=agent_system_prompt,
593
593
  chat_mode=chat_mode, stream_response=response,
594
594
  )
595
+ elif self.core.main_agent and self.core.llm:
596
+ # model_chain is empty (no model_id configured), but LLM is available
597
+ # Use _stream_process_message for true token-by-token streaming
598
+ full_response = await self._stream_process_message(
599
+ clean_message, session_id, response,
600
+ agent_path=agent_path, agent_system_prompt=agent_system_prompt,
601
+ chat_mode=chat_mode,
602
+ )
595
603
  else:
604
+ # No LLM at all — non-streaming fallback
596
605
  full_response = await self.core.process_message(clean_message, session_id)
597
606
  await response.write(("data: " + json.dumps({"type": "text", "content": full_response}) + "\n\n").encode())
598
607
 
@@ -640,6 +649,12 @@ class ApiServer:
640
649
  agent_path=agent_path, agent_system_prompt=agent_system_prompt_q,
641
650
  chat_mode=chat_mode, stream_response=response,
642
651
  )
652
+ elif self.core.main_agent and self.core.llm:
653
+ full_response = await self._stream_process_message(
654
+ clean_message_q, session_id, response,
655
+ agent_path=agent_path, agent_system_prompt=agent_system_prompt_q,
656
+ chat_mode=chat_mode,
657
+ )
643
658
  else:
644
659
  full_response = await self.core.process_message(clean_message_q, session_id)
645
660
  await response.write(("data: " + json.dumps({"type": "text", "content": full_response}) + "\n\n").encode())
@@ -3870,6 +3885,7 @@ class ApiServer:
3870
3885
  # 获取agent的显示信息
3871
3886
  avatar = "🤖"
3872
3887
  display_name = agent_path
3888
+ agent_color = "var(--accent)"
3873
3889
  if agent_cfg:
3874
3890
  avatar = agent_cfg.get("avatar_emoji", "🤖") or "🤖"
3875
3891
  display_name = agent_cfg.get("name", agent_path)
@@ -3879,6 +3895,7 @@ class ApiServer:
3879
3895
  "agent_path": agent_path,
3880
3896
  "name": display_name,
3881
3897
  "avatar": avatar,
3898
+ "agent_color": agent_color,
3882
3899
  "response": response,
3883
3900
  }
3884
3901
  except Exception as e:
package/web/ui/chat.html CHANGED
@@ -3351,6 +3351,17 @@ function renderMessages() {
3351
3351
  <div class="thought-content">${renderMarkdown(msg.thought)}</div>
3352
3352
  </details>`;
3353
3353
  })() : '';
3354
+ const reasoningHtml = msg.reasoning ? (() => {
3355
+ const isStreaming = !!msg.streaming;
3356
+ return `<details class="thought-block ${isStreaming ? 'streaming' : ''}" ${isStreaming ? 'open' : ''}>
3357
+ <summary>
3358
+ <span class="thought-icon">💡</span>
3359
+ <span class="thought-label">模型推理过程</span>
3360
+ ${isStreaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
3361
+ </summary>
3362
+ <div class="thought-content">${renderMarkdown(msg.reasoning)}</div>
3363
+ </details>`;
3364
+ })() : '';
3354
3365
  const actionBtns = (!isUser && msg.content) ? `
3355
3366
  <div class="msg-actions">
3356
3367
  <button class="msg-action-btn" onclick="copyMessage(${i})" title="复制">
@@ -3376,6 +3387,7 @@ function renderMessages() {
3376
3387
  <div class="message-row ${msg.role}">
3377
3388
  <div class="message-avatar">${avatar}</div>
3378
3389
  <div style="flex:1;min-width:0">
3390
+ ${reasoningHtml}
3379
3391
  ${thoughtHtml}
3380
3392
  ${content || streamingIndicator ? `<div class="message-bubble">${content}${ttsIndicator}</div>` : ''}
3381
3393
  ${streamingIndicator}
@@ -3393,6 +3405,165 @@ function renderMessages() {
3393
3405
  scrollToBottom();
3394
3406
  }
3395
3407
 
3408
+ // ── Incremental Streaming Update (avoid full DOM rebuild on every token) ──
3409
+ function updateStreamingMessage(msgIdx) {
3410
+ // Only update the last streaming message bubble without rebuilding the entire DOM
3411
+ const msg = state.messages[msgIdx];
3412
+ if (!msg || msg.role !== 'assistant') return;
3413
+
3414
+ const container = document.getElementById('messagesInner');
3415
+ if (!container) return;
3416
+
3417
+ // Find or create the streaming message row
3418
+ const rows = container.querySelectorAll('.message-row.assistant');
3419
+ let targetRow = null;
3420
+ // Count assistant rows to find the right one
3421
+ let assistantCount = 0;
3422
+ for (const row of rows) {
3423
+ assistantCount++;
3424
+ if (assistantCount === msgIdx + 1) {
3425
+ targetRow = row;
3426
+ break;
3427
+ }
3428
+ }
3429
+
3430
+ // Fallback: if we can't find by count, use last assistant row
3431
+ if (!targetRow && rows.length > 0) {
3432
+ targetRow = rows[rows.length - 1];
3433
+ }
3434
+
3435
+ if (!targetRow) {
3436
+ // Message row doesn't exist yet, fall back to full render
3437
+ renderMessages();
3438
+ return;
3439
+ }
3440
+
3441
+ const contentArea = targetRow.querySelector(':scope > div');
3442
+ if (!contentArea) return;
3443
+
3444
+ // Update reasoning block (model inference/reasoning - e.g. o1, DeepSeek-R1)
3445
+ let reasoningBlock = contentArea.querySelector('.thought-block .thought-label');
3446
+ // Find reasoning block by looking for "模型推理过程" label
3447
+ let reasoningDetails = null;
3448
+ const allThoughtBlocks = contentArea.querySelectorAll('.thought-block');
3449
+ for (const tb of allThoughtBlocks) {
3450
+ const label = tb.querySelector('.thought-label');
3451
+ if (label && label.textContent.includes('模型推理过程')) {
3452
+ reasoningDetails = tb;
3453
+ break;
3454
+ }
3455
+ }
3456
+ if (msg.reasoning) {
3457
+ const reasoningHtml = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
3458
+ <summary>
3459
+ <span class="thought-icon">💡</span>
3460
+ <span class="thought-label">模型推理过程</span>
3461
+ ${msg.streaming ? '<span class="thought-badge">推理中...</span>' : '<span class="thought-badge">已完成</span>'}
3462
+ </summary>
3463
+ <div class="thought-content">${renderMarkdown(msg.reasoning)}</div>
3464
+ </details>`;
3465
+ if (reasoningDetails) {
3466
+ reasoningDetails.outerHTML = reasoningHtml;
3467
+ } else {
3468
+ contentArea.insertAdjacentHTML('afterbegin', reasoningHtml);
3469
+ }
3470
+ }
3471
+
3472
+ // Update thought block (agent thinking)
3473
+ let thoughtBlock = null;
3474
+ const updatedBlocks = contentArea.querySelectorAll('.thought-block');
3475
+ for (const tb of updatedBlocks) {
3476
+ const label = tb.querySelector('.thought-label');
3477
+ if (label && label.textContent.includes('Agent 思考过程')) {
3478
+ thoughtBlock = tb;
3479
+ break;
3480
+ }
3481
+ }
3482
+ if (msg.thought) {
3483
+ const thoughtHtml = `<details class="thought-block ${msg.streaming ? 'streaming' : ''}" ${msg.streaming ? 'open' : ''}>
3484
+ <summary>
3485
+ <span class="thought-icon">🧠</span>
3486
+ <span class="thought-label">Agent 思考过程</span>
3487
+ ${msg.streaming ? '<span class="thought-badge">思考中...</span>' : '<span class="thought-badge">已完成</span>'}
3488
+ </summary>
3489
+ <div class="thought-content">${renderMarkdown(msg.thought)}</div>
3490
+ </details>`;
3491
+ if (thoughtBlock) {
3492
+ thoughtBlock.outerHTML = thoughtHtml;
3493
+ } else {
3494
+ contentArea.insertAdjacentHTML('afterbegin', thoughtHtml);
3495
+ }
3496
+ } else if (thoughtBlock && !msg.thought) {
3497
+ thoughtBlock.remove();
3498
+ }
3499
+
3500
+ // Update message bubble content
3501
+ const bubble = contentArea.querySelector('.message-bubble');
3502
+ const content = msg.content ? renderMarkdown(msg.content) : '';
3503
+ const streamingIndicator = msg.streaming && !msg.content && !msg.thought ? `
3504
+ <div class="streaming-indicator">
3505
+ <div class="spinner"></div>
3506
+ <div class="streaming-dots">
3507
+ <span class="dot"></span><span class="dot"></span><span class="dot"></span>
3508
+ </div>
3509
+ <span style="font-weight:500">Agent 正在思考...</span>
3510
+ </div>` : '';
3511
+
3512
+ if (bubble) {
3513
+ if (content) {
3514
+ bubble.innerHTML = content;
3515
+ bubble.style.display = '';
3516
+ } else {
3517
+ bubble.style.display = 'none';
3518
+ }
3519
+ } else if (content) {
3520
+ contentArea.insertAdjacentHTML('afterbegin', `<div class="message-bubble">${content}</div>`);
3521
+ }
3522
+
3523
+ // Update streaming indicator
3524
+ let indicator = contentArea.querySelector('.streaming-indicator');
3525
+ if (streamingIndicator) {
3526
+ if (!indicator) {
3527
+ contentArea.insertAdjacentHTML('beforeend', streamingIndicator);
3528
+ }
3529
+ } else if (indicator) {
3530
+ indicator.remove();
3531
+ }
3532
+
3533
+ // Update exec events
3534
+ const existingPanel = contentArea.querySelector('.exec-events-panel');
3535
+ if (msg.exec_events && msg.exec_events.length > 0) {
3536
+ const execHtml = renderExecEvents(msg.exec_events, msgIdx);
3537
+ if (existingPanel) {
3538
+ existingPanel.outerHTML = execHtml;
3539
+ } else {
3540
+ contentArea.insertAdjacentHTML('beforeend', execHtml);
3541
+ }
3542
+ }
3543
+
3544
+ // Auto-scroll
3545
+ scrollToBottom();
3546
+ }
3547
+
3548
+ // Throttle streaming updates (max ~20fps to avoid excessive reflows)
3549
+ const _streamThrottle = { last: 0, pending: false, idx: -1 };
3550
+ function throttledStreamUpdate(msgIdx) {
3551
+ _streamThrottle.idx = msgIdx;
3552
+ if (_streamThrottle.pending) return;
3553
+ const now = performance.now();
3554
+ if (now - _streamThrottle.last < 50) {
3555
+ _streamThrottle.pending = true;
3556
+ requestAnimationFrame(() => {
3557
+ _streamThrottle.pending = false;
3558
+ _streamThrottle.last = performance.now();
3559
+ updateStreamingMessage(_streamThrottle.idx);
3560
+ });
3561
+ return;
3562
+ }
3563
+ _streamThrottle.last = now;
3564
+ updateStreamingMessage(msgIdx);
3565
+ }
3566
+
3396
3567
  // ── Execution Events Rendering ──
3397
3568
  function renderExecEvents(events, msgIndex) {
3398
3569
  // 过滤出有意义的事件(仅展示调用/结果对,跳过中间状态)
@@ -3789,6 +3960,8 @@ async function sendMessage() {
3789
3960
 
3790
3961
  if (evt.type === 'session') {
3791
3962
  sessionIdReceived = evt.session_id;
3963
+ // Sync the actual session ID (backend may prefix with agent_path)
3964
+ state.activeSessionId = evt.session_id;
3792
3965
  } else if (evt.type === 'text') {
3793
3966
  fullResponse = evt.content;
3794
3967
  state.messages[msgIdx].content = evt.content;
@@ -3797,12 +3970,12 @@ async function sendMessage() {
3797
3970
  // Incremental streaming token
3798
3971
  fullResponse += evt.content;
3799
3972
  state.messages[msgIdx].content = fullResponse;
3800
- renderMessages();
3973
+ throttledStreamUpdate(msgIdx);
3801
3974
  } else if (evt.type === 'thought_delta') {
3802
3975
  // Agent 思考过程增量文本(流式推送,单独显示)
3803
3976
  fullThought += evt.content;
3804
3977
  state.messages[msgIdx].thought = fullThought;
3805
- renderMessages();
3978
+ throttledStreamUpdate(msgIdx);
3806
3979
  } else if (evt.type === 'thought') {
3807
3980
  // Agent 思考过程文本
3808
3981
  if (fullThought.trim() === '') {
@@ -3811,7 +3984,7 @@ async function sendMessage() {
3811
3984
  fullThought += '\n\n' + evt.content;
3812
3985
  }
3813
3986
  state.messages[msgIdx].thought = fullThought;
3814
- renderMessages();
3987
+ throttledStreamUpdate(msgIdx);
3815
3988
  } else if (evt.type === 'queue_start') {
3816
3989
  // New message starting from queue
3817
3990
  if (state.messages[msgIdx]) {
@@ -3829,13 +4002,13 @@ async function sendMessage() {
3829
4002
  // Clear intermediate text from previous agent loop iterations
3830
4003
  fullResponse = '';
3831
4004
  state.messages[msgIdx].content = '';
3832
- renderMessages();
4005
+ throttledStreamUpdate(msgIdx);
3833
4006
  } else if (evt.type === 'exec_event') {
3834
4007
  // Real-time execution event (tool call, code exec, skill result, etc.)
3835
4008
  execEventsReceived.push(evt.data);
3836
4009
  // 立即更新消息的 exec_events 并渲染
3837
4010
  state.messages[msgIdx].exec_events = [...execEventsReceived];
3838
- renderMessages();
4011
+ throttledStreamUpdate(msgIdx);
3839
4012
  } else if (evt.type === 'task_plan_updated') {
3840
4013
  // 任务计划已更新,刷新侧边栏任务列表
3841
4014
  loadTaskPlan();
@@ -3845,6 +4018,15 @@ async function sendMessage() {
3845
4018
  execEventsReceived = evt.exec_events;
3846
4019
  state.messages[msgIdx].exec_events = [...execEventsReceived];
3847
4020
  }
4021
+ } else if (evt.type === 'reasoning_delta') {
4022
+ // 模型推理过程增量文本(OpenAI o1/o3/DeepSeek-R1 等推理模型)
4023
+ if (!state.messages[msgIdx].reasoning) state.messages[msgIdx].reasoning = '';
4024
+ state.messages[msgIdx].reasoning += evt.content;
4025
+ throttledStreamUpdate(msgIdx);
4026
+ } else if (evt.type === 'reasoning') {
4027
+ // 模型推理完整文本
4028
+ state.messages[msgIdx].reasoning = evt.content;
4029
+ throttledStreamUpdate(msgIdx);
3848
4030
  } else if (evt.type === 'error') {
3849
4031
  fullResponse = '❌ ' + evt.error;
3850
4032
  state.messages[msgIdx].content = fullResponse;
@@ -4770,7 +4952,22 @@ async function selectGroup(gid) {
4770
4952
 
4771
4953
  // Load messages
4772
4954
  var msgsData = await getGroupMessages(gid);
4773
- groupMessages = msgsData || [];
4955
+ // Normalize backend message format: map sender->role, sender_name->agent_name, sender_avatar->agent_emoji
4956
+ groupMessages = (msgsData || []).map(function(m) {
4957
+ if (m.sender) {
4958
+ return {
4959
+ role: m.sender === 'user' ? 'user' : 'assistant',
4960
+ agent: m.agent_path || '',
4961
+ agent_name: m.sender_name || '',
4962
+ agent_emoji: m.sender_avatar || '🤖',
4963
+ agent_color: m.agent_color || '',
4964
+ content: m.content || '',
4965
+ time: m.timestamp ? new Date(m.timestamp * 1000).toISOString() : (m.time || ''),
4966
+ type: m.msg_type === 'system' ? 'system' : '',
4967
+ };
4968
+ }
4969
+ return m;
4970
+ });
4774
4971
  renderGroupMessages();
4775
4972
  } catch (e) {
4776
4973
  toast('加载群聊失败: ' + e.message, 'error');
@@ -4847,8 +5044,9 @@ function renderGroupMessages() {
4847
5044
  // Multiple agent responses (broadcast response)
4848
5045
  for (var j = 0; j < msg.responses.length; j++) {
4849
5046
  var r = msg.responses[j];
4850
- var rName = r.agent_name || r.agent || 'Agent';
4851
- var rEmoji = r.agent_emoji || '🤖';
5047
+ // Backend returns "name" and "avatar" fields, with "agent_name"/"agent_emoji" as fallback
5048
+ var rName = r.name || r.agent_name || r.agent || 'Agent';
5049
+ var rEmoji = r.avatar || r.agent_emoji || '🤖';
4852
5050
  var rColor = r.agent_color || 'var(--accent)';
4853
5051
  html += '<div class="group-msg-row">'
4854
5052
  + '<div class="group-msg-avatar" style="background:' + rColor + ';color:#fff">' + rEmoji + '</div>'