Compare commits
30 Commits
FKING-GLAS
...
5470f2106f
| Author | SHA1 | Date | |
|---|---|---|---|
| 5470f2106f | |||
| 0f62eaff6d | |||
| 5285bc68f9 | |||
| 226ffdbd2a | |||
| 6594a50e4e | |||
| 1a305ee614 | |||
| 81ded98198 | |||
| b85b7d9700 | |||
| 3d0c40de45 | |||
| 47c5100ec5 | |||
| bc00fe1197 | |||
| 9515dee44d | |||
| 13199a0008 | |||
| 45c9e15a3c | |||
| d18eabdf4d | |||
| 9fb8b5757f | |||
| e30cbb5047 | |||
| 017a52a90a | |||
| 71269ceb97 | |||
| 0b33cbe023 | |||
| 1164aefffa | |||
| 1ad146b38e | |||
| 084f9429af | |||
| 95e6413017 | |||
| fc7b491f78 | |||
| 44a1d76dc7 | |||
| ea7b3ae3ae | |||
| c5a406eff8 | |||
| c15f38fb09 | |||
| 645f71d674 |
@@ -35,8 +35,8 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
7. [ ] **Track: Optimization pass for Data-Oriented Python heuristics**
|
||||
*Link: [./tracks/data_oriented_optimization_20260312/](./tracks/data_oriented_optimization_20260312/)*
|
||||
|
||||
8. [ ] **Track: Rich Thinking Trace Handling**
|
||||
*Link: [./tracks/thinking_trace_handling_20260313/](./tracks/thinking_trace_handling_20260313/)*
|
||||
8. [x] **Track: Rich Thinking Trace Handling** - *Parse and display AI thinking/reasoning traces*
|
||||
*Link: [./tracks/thinking_trace_handling_20260313/](./tracks/thinking_trace_handling_20260313/)*
|
||||
|
||||
---
|
||||
|
||||
@@ -60,7 +60,7 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
|
||||
5. [x] **Track: NERV UI Theme Integration** (Archived 2026-03-09)
|
||||
|
||||
6. [ ] **Track: Custom Shader and Window Frame Support**
|
||||
6. [X] **Track: Custom Shader and Window Frame Support**
|
||||
*Link: [./tracks/custom_shaders_20260309/](./tracks/custom_shaders_20260309/)*
|
||||
|
||||
7. [x] **Track: UI/UX Improvements - Presets and AI Settings**
|
||||
@@ -82,9 +82,6 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
11. [ ] **Track: Advanced Text Viewer with Syntax Highlighting**
|
||||
*Link: [./tracks/text_viewer_rich_rendering_20260313/](./tracks/text_viewer_rich_rendering_20260313/)*
|
||||
|
||||
12. [ ] **Track: Frosted Glass Background Effect**
|
||||
*Link: [./tracks/frosted_glass_20260313/](./tracks/frosted_glass_20260313/)*
|
||||
|
||||
---
|
||||
|
||||
### Additional Language Support
|
||||
@@ -164,6 +161,10 @@ This file tracks all major tracks for the project. Each track has its own detail
|
||||
|
||||
### Completed / Archived
|
||||
|
||||
-. [ ] ~~**Track: Frosted Glass Background Effect**~~ ***NOT WORTH THE PAIN***
|
||||
*Link: [./tracks/frosted_glass_20260313/](./tracks/frosted_glass_20260313/)*
|
||||
|
||||
|
||||
- [x] **Track: External MCP Server Support** (Archived 2026-03-12)
|
||||
- [x] **Track: Project-Specific Conductor Directory** (Archived 2026-03-12)
|
||||
- [x] **Track: GUI Path Configuration in Context Hub** (Archived 2026-03-12)
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
# Implementation Plan: Rich Thinking Trace Handling
|
||||
|
||||
## Phase 1: Core Parsing & Model Update
|
||||
- [ ] Task: Audit `src/models.py` and `src/project_manager.py` to identify current message serialization schemas.
|
||||
- [ ] Task: Write Tests: Verify that raw AI responses with `<thinking>`, `<thought>`, and `Thinking:` markers are correctly parsed into segmented data structures (Thinking vs. Response).
|
||||
- [ ] Task: Implement: Add `ThinkingSegment` model and update `ChatMessage` schema in `src/models.py` to support optional thinking traces.
|
||||
- [ ] Task: Implement: Update parsing logic in `src/ai_client.py` or a dedicated utility to extract segments from raw provider responses.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 1: Core Parsing & Model Update' (Protocol in workflow.md)
|
||||
## Status: COMPLETE (2026-03-14)
|
||||
|
||||
## Phase 2: Persistence & History Integration
|
||||
- [ ] Task: Write Tests: Verify that `ProjectManager` correctly serializes and deserializes messages with thinking segments to/from TOML history files.
|
||||
- [ ] Task: Implement: Update `src/project_manager.py` to handle the new `ChatMessage` schema during session save/load.
|
||||
- [ ] Task: Implement: Ensure `src/aggregate.py` or relevant context builders include thinking traces in the "Discussion History" sent back to the AI.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 2: Persistence & History Integration' (Protocol in workflow.md)
|
||||
## Summary
|
||||
Implemented thinking trace parsing, model, persistence, and GUI rendering for AI responses containing `<thinking>`, `<thought>`, and `Thinking:` markers.
|
||||
|
||||
## Phase 3: GUI Rendering - Comms & Discussion
|
||||
- [ ] Task: Write Tests: Verify the GUI rendering logic correctly handles messages with and without thinking segments.
|
||||
- [ ] Task: Implement: Create a reusable `_render_thinking_trace` helper in `src/gui_2.py` using a collapsible header (e.g., `imgui.collapsing_header`).
|
||||
- [ ] Task: Implement: Integrate the thinking trace renderer into the **Comms History** panel in `src/gui_2.py`.
|
||||
- [ ] Task: Implement: Integrate the thinking trace renderer into the **Discussion Hub** message loop in `src/gui_2.py`.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 3: GUI Rendering - Comms & Discussion' (Protocol in workflow.md)
|
||||
## Files Created/Modified:
|
||||
- `src/thinking_parser.py` - Parser for thinking traces
|
||||
- `src/models.py` - ThinkingSegment model
|
||||
- `src/gui_2.py` - _render_thinking_trace helper + integration
|
||||
- `tests/test_thinking_trace.py` - 7 parsing tests
|
||||
- `tests/test_thinking_persistence.py` - 4 persistence tests
|
||||
- `tests/test_thinking_gui.py` - 4 GUI tests
|
||||
|
||||
## Phase 4: Final Polish & Theming
|
||||
- [ ] Task: Implement: Apply specialized styling (e.g., tinted background or italicized text) to expanded thinking traces to distinguish them from direct responses.
|
||||
- [ ] Task: Implement: Ensure thinking trace headers show a "Calculating..." or "Monologue" indicator while an agent is active.
|
||||
- [ ] Task: Conductor - User Manual Verification 'Phase 4: Final Polish & Theming' (Protocol in workflow.md)
|
||||
## Implementation Details:
|
||||
- **Parser**: Extracts thinking segments from `<thinking>`, `<thought>`, `Thinking:` markers
|
||||
- **Model**: `ThinkingSegment` dataclass with content and marker fields
|
||||
- **GUI**: `_render_thinking_trace` with collapsible "Monologue" header
|
||||
- **Styling**: Tinted background (dark brown), gold/amber text
|
||||
- **Indicator**: Existing "THINKING..." in Discussion Hub
|
||||
|
||||
## Total Tests: 15 passing
|
||||
|
||||
10
config.toml
10
config.toml
@@ -37,7 +37,7 @@ separate_external_tools = false
|
||||
"Context Hub" = true
|
||||
"Files & Media" = true
|
||||
"AI Settings" = true
|
||||
"MMA Dashboard" = true
|
||||
"MMA Dashboard" = false
|
||||
"Task DAG" = false
|
||||
"Usage Analytics" = false
|
||||
"Tier 1" = false
|
||||
@@ -51,10 +51,10 @@ separate_external_tools = false
|
||||
"Discussion Hub" = true
|
||||
"Operations Hub" = true
|
||||
Message = false
|
||||
Response = true
|
||||
Response = false
|
||||
"Tool Calls" = false
|
||||
Theme = true
|
||||
"Log Management" = true
|
||||
"Log Management" = false
|
||||
Diagnostics = false
|
||||
"External Tools" = false
|
||||
"Shader Editor" = false
|
||||
@@ -64,8 +64,8 @@ palette = "Nord Dark"
|
||||
font_path = "C:/projects/manual_slop/assets/fonts/MapleMono-Regular.ttf"
|
||||
font_size = 18.0
|
||||
scale = 1.0
|
||||
transparency = 0.5400000214576721
|
||||
child_transparency = 0.5899999737739563
|
||||
transparency = 1.0
|
||||
child_transparency = 1.0
|
||||
|
||||
[mma]
|
||||
max_workers = 4
|
||||
|
||||
@@ -49,8 +49,8 @@ Size=716,455
|
||||
Collapsed=0
|
||||
|
||||
[Window][Response]
|
||||
Pos=2437,925
|
||||
Size=1111,773
|
||||
Pos=2258,1377
|
||||
Size=1102,575
|
||||
Collapsed=0
|
||||
|
||||
[Window][Tool Calls]
|
||||
@@ -74,8 +74,8 @@ Collapsed=0
|
||||
DockId=0xAFC85805,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,703
|
||||
Size=630,737
|
||||
Pos=0,1423
|
||||
Size=579,737
|
||||
Collapsed=0
|
||||
DockId=0x00000002,2
|
||||
|
||||
@@ -91,8 +91,8 @@ Collapsed=0
|
||||
DockId=0x00000010,2
|
||||
|
||||
[Window][Context Hub]
|
||||
Pos=0,703
|
||||
Size=630,737
|
||||
Pos=0,1423
|
||||
Size=579,737
|
||||
Collapsed=0
|
||||
DockId=0x00000002,1
|
||||
|
||||
@@ -103,26 +103,26 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=1263,22
|
||||
Size=709,1418
|
||||
Pos=2230,26
|
||||
Size=1610,2134
|
||||
Collapsed=0
|
||||
DockId=0x00000013,0
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=632,22
|
||||
Size=629,1418
|
||||
Pos=581,26
|
||||
Size=1647,2134
|
||||
Collapsed=0
|
||||
DockId=0x00000005,0
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,703
|
||||
Size=630,737
|
||||
Pos=0,1423
|
||||
Size=579,737
|
||||
Collapsed=0
|
||||
DockId=0x00000002,0
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,22
|
||||
Size=630,679
|
||||
Pos=0,26
|
||||
Size=579,1395
|
||||
Collapsed=0
|
||||
DockId=0x00000001,0
|
||||
|
||||
@@ -132,16 +132,16 @@ Size=416,325
|
||||
Collapsed=0
|
||||
|
||||
[Window][MMA Dashboard]
|
||||
Pos=1974,22
|
||||
Size=586,1418
|
||||
Pos=3360,26
|
||||
Size=480,2134
|
||||
Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Log Management]
|
||||
Pos=1974,22
|
||||
Size=586,1418
|
||||
Pos=3360,26
|
||||
Size=480,2134
|
||||
Collapsed=0
|
||||
DockId=0x00000010,1
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Track Proposal]
|
||||
Pos=709,326
|
||||
@@ -175,7 +175,7 @@ Size=381,329
|
||||
Collapsed=0
|
||||
|
||||
[Window][Last Script Output]
|
||||
Pos=2810,265
|
||||
Pos=927,1365
|
||||
Size=800,562
|
||||
Collapsed=0
|
||||
|
||||
@@ -220,7 +220,7 @@ Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
[Window][Text Viewer - text]
|
||||
Pos=60,60
|
||||
Pos=1297,550
|
||||
Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
@@ -366,7 +366,7 @@ Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
[Window][Text Viewer - Entry #4]
|
||||
Pos=1127,922
|
||||
Pos=1247,1182
|
||||
Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
@@ -498,19 +498,19 @@ Column 1 Weight=1.0000
|
||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,22 Size=2560,1418 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=1640,1183 Split=X
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,26 Size=3840,2134 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=3358,1183 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=630,858 Split=Y Selected=0x8CA2375C
|
||||
DockNode ID=0x00000007 Parent=0x0000000B SizeRef=579,858 Split=Y Selected=0x8CA2375C
|
||||
DockNode ID=0x00000001 Parent=0x00000007 SizeRef=824,525 CentralNode=1 Selected=0x7BD57D6A
|
||||
DockNode ID=0x00000002 Parent=0x00000007 SizeRef=824,737 Selected=0x8CA2375C
|
||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=1340,858 Split=X Selected=0x418C7449
|
||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=629,402 Split=Y Selected=0x418C7449
|
||||
DockNode ID=0x0000000E Parent=0x0000000B SizeRef=3259,858 Split=X Selected=0x418C7449
|
||||
DockNode ID=0x00000012 Parent=0x0000000E SizeRef=1647,402 Split=Y Selected=0x418C7449
|
||||
DockNode ID=0x00000005 Parent=0x00000012 SizeRef=876,1749 Selected=0x418C7449
|
||||
DockNode ID=0x00000006 Parent=0x00000012 SizeRef=876,362 Selected=0x1D56B311
|
||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=709,402 Selected=0x6F2B5B04
|
||||
DockNode ID=0x00000013 Parent=0x0000000E SizeRef=1610,402 Selected=0x6F2B5B04
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=586,1183 Split=Y Selected=0x3AEC3498
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=480,1183 Split=Y Selected=0x3AEC3498
|
||||
DockNode ID=0x00000010 Parent=0x00000004 SizeRef=1199,1689 Selected=0x2C0206CE
|
||||
DockNode ID=0x00000011 Parent=0x00000004 SizeRef=1199,420 Split=X Selected=0xDEB547B6
|
||||
DockNode ID=0x0000000C Parent=0x00000011 SizeRef=916,380 Selected=0x655BC6E9
|
||||
|
||||
@@ -25,6 +25,7 @@ from src import project_manager
|
||||
from src import performance_monitor
|
||||
from src import models
|
||||
from src import presets
|
||||
from src import thinking_parser
|
||||
from src.file_cache import ASTParser
|
||||
from src import ai_client
|
||||
from src import shell_runner
|
||||
@@ -610,16 +611,6 @@ class AppController:
|
||||
self._token_stats_dirty = True
|
||||
if not is_streaming:
|
||||
self._autofocus_response_tab = True
|
||||
# ONLY add to history when turn is complete
|
||||
if self.ui_auto_add_history and not stream_id and not is_streaming:
|
||||
role = payload.get("role", "AI")
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({
|
||||
"role": role,
|
||||
"content": self.ai_response,
|
||||
"collapsed": True,
|
||||
"ts": project_manager.now_ts()
|
||||
})
|
||||
elif action in ("mma_stream", "mma_stream_append"):
|
||||
# Some events might have these at top level, some in a 'payload' dict
|
||||
stream_id = task.get("stream_id") or task.get("payload", {}).get("stream_id")
|
||||
@@ -1467,9 +1458,22 @@ class AppController:
|
||||
|
||||
if kind == "response" and "usage" in payload:
|
||||
u = payload["usage"]
|
||||
for k in ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "total_tokens"]:
|
||||
if k in u:
|
||||
self.session_usage[k] += u.get(k, 0) or 0
|
||||
inp = u.get("input_tokens", u.get("prompt_tokens", 0))
|
||||
out = u.get("output_tokens", u.get("completion_tokens", 0))
|
||||
cache_read = u.get("cache_read_input_tokens", 0)
|
||||
cache_create = u.get("cache_creation_input_tokens", 0)
|
||||
total = u.get("total_tokens", 0)
|
||||
|
||||
# Store normalized usage back in payload for history rendering
|
||||
u["input_tokens"] = inp
|
||||
u["output_tokens"] = out
|
||||
u["cache_read_input_tokens"] = cache_read
|
||||
|
||||
self.session_usage["input_tokens"] += inp
|
||||
self.session_usage["output_tokens"] += out
|
||||
self.session_usage["cache_read_input_tokens"] += cache_read
|
||||
self.session_usage["cache_creation_input_tokens"] += cache_create
|
||||
self.session_usage["total_tokens"] += total
|
||||
input_t = u.get("input_tokens", 0)
|
||||
output_t = u.get("output_tokens", 0)
|
||||
model = payload.get("model", "unknown")
|
||||
@@ -1490,22 +1494,42 @@ class AppController:
|
||||
"ts": entry.get("ts", project_manager.now_ts())
|
||||
})
|
||||
|
||||
if kind in ("tool_result", "tool_call"):
|
||||
role = "Tool" if kind == "tool_result" else "Vendor API"
|
||||
content = ""
|
||||
if kind == "tool_result":
|
||||
content = payload.get("output", "")
|
||||
else:
|
||||
content = payload.get("script") or payload.get("args") or payload.get("message", "")
|
||||
if isinstance(content, dict):
|
||||
content = json.dumps(content, indent=1)
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({
|
||||
if kind == "response":
|
||||
if self.ui_auto_add_history:
|
||||
role = payload.get("role", "AI")
|
||||
text_content = payload.get("text", "")
|
||||
if text_content.strip():
|
||||
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
|
||||
entry_obj = {
|
||||
"role": role,
|
||||
"content": f"[{kind.upper().replace('_', ' ')}]\n{content}",
|
||||
"content": parsed_response.strip() if parsed_response else "",
|
||||
"collapsed": True,
|
||||
"ts": entry.get("ts", project_manager.now_ts())
|
||||
})
|
||||
}
|
||||
if segments:
|
||||
entry_obj["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
|
||||
|
||||
if entry_obj["content"] or segments:
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append(entry_obj)
|
||||
|
||||
if kind in ("tool_result", "tool_call"):
|
||||
if self.ui_auto_add_history:
|
||||
role = "Tool" if kind == "tool_result" else "Vendor API"
|
||||
content = ""
|
||||
if kind == "tool_result":
|
||||
content = payload.get("output", "")
|
||||
else:
|
||||
content = payload.get("script") or payload.get("args") or payload.get("message", "")
|
||||
if isinstance(content, dict):
|
||||
content = json.dumps(content, indent=1)
|
||||
with self._pending_history_adds_lock:
|
||||
self._pending_history_adds.append({
|
||||
"role": role,
|
||||
"content": f"[{kind.upper().replace('_', ' ')}]\n{content}",
|
||||
"collapsed": True,
|
||||
"ts": entry.get("ts", project_manager.now_ts())
|
||||
})
|
||||
if kind == "history_add":
|
||||
payload = entry.get("payload", {})
|
||||
with self._pending_history_adds_lock:
|
||||
|
||||
177
src/gui_2.py
177
src/gui_2.py
@@ -28,6 +28,8 @@ from src import app_controller
|
||||
from src import mcp_client
|
||||
from src import markdown_helper
|
||||
from src import bg_shader
|
||||
from src import thinking_parser
|
||||
from src import thinking_parser
|
||||
import re
|
||||
import subprocess
|
||||
if sys.platform == "win32":
|
||||
@@ -304,21 +306,57 @@ class App:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
|
||||
if len(content) > COMMS_CLAMP_CHARS:
|
||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True)
|
||||
if is_md:
|
||||
imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar)
|
||||
markdown_helper.render(content, context_id=ctx_id)
|
||||
imgui.end_child()
|
||||
else:
|
||||
markdown_helper.render_code(content, context_id=ctx_id)
|
||||
imgui.end_child()
|
||||
imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only)
|
||||
else:
|
||||
if is_md:
|
||||
markdown_helper.render(content, context_id=ctx_id)
|
||||
else:
|
||||
markdown_helper.render_code(content, context_id=ctx_id)
|
||||
if self.ui_word_wrap:
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text(content)
|
||||
imgui.pop_text_wrap_pos()
|
||||
else:
|
||||
imgui.text(content)
|
||||
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
# ---------------------------------------------------------------- gui
|
||||
|
||||
def _render_thinking_trace(self, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None:
|
||||
if not segments:
|
||||
return
|
||||
imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180))
|
||||
imgui.push_style_color(imgui.Col_.text, vec4(200, 200, 150))
|
||||
imgui.indent()
|
||||
show_content = True
|
||||
if not is_standalone:
|
||||
header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}"
|
||||
show_content = imgui.collapsing_header(header_label)
|
||||
|
||||
if show_content:
|
||||
h = 150 if is_standalone else 100
|
||||
imgui.begin_child(f"thinking_content_{entry_index}", imgui.ImVec2(0, h), True)
|
||||
for idx, seg in enumerate(segments):
|
||||
content = seg.get("content", "")
|
||||
marker = seg.get("marker", "thinking")
|
||||
imgui.push_id(f"think_{entry_index}_{idx}")
|
||||
imgui.text_colored(vec4(180, 150, 80), f"[{marker}]")
|
||||
if self.ui_word_wrap:
|
||||
imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
imgui.text_colored(vec4(200, 200, 150), content)
|
||||
imgui.pop_text_wrap_pos()
|
||||
else:
|
||||
imgui.text_colored(vec4(200, 200, 150), content)
|
||||
imgui.pop_id()
|
||||
imgui.separator()
|
||||
imgui.end_child()
|
||||
imgui.unindent()
|
||||
imgui.pop_style_color(2)
|
||||
|
||||
|
||||
def _render_selectable_label(self, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None:
|
||||
imgui.push_id(label + str(hash(value)))
|
||||
@@ -2251,52 +2289,61 @@ def hello():
|
||||
imgui.same_line()
|
||||
preview = entry["content"].replace("\\n", " ")[:60]
|
||||
if len(entry["content"]) > 60: preview += "..."
|
||||
if not preview.strip() and entry.get("thinking_segments"):
|
||||
preview = entry["thinking_segments"][0]["content"].replace("\\n", " ")[:60]
|
||||
if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..."
|
||||
imgui.text_colored(vec4(160, 160, 150), preview)
|
||||
if not collapsed:
|
||||
thinking_segments = entry.get("thinking_segments", [])
|
||||
has_content = bool(entry.get("content", "").strip())
|
||||
is_standalone = bool(thinking_segments) and not has_content
|
||||
if thinking_segments:
|
||||
self._render_thinking_trace(thinking_segments, i, is_standalone=is_standalone)
|
||||
if read_mode:
|
||||
content = entry["content"]
|
||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||
matches = list(pattern.finditer(content))
|
||||
is_nerv = theme.is_nerv_active()
|
||||
if not matches:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(content, context_id=f'disc_{i}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
else:
|
||||
imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True)
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
last_idx = 0
|
||||
for m_idx, match in enumerate(matches):
|
||||
before = content[last_idx:match.start()]
|
||||
if before:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
header_text = match.group(0).split("\n")[0].strip()
|
||||
path = match.group(2)
|
||||
code_block = match.group(4)
|
||||
if imgui.collapsing_header(header_text):
|
||||
if imgui.button(f"[Source]##{i}_{match.start()}"):
|
||||
res = mcp_client.read_file(path)
|
||||
if res:
|
||||
self.text_viewer_title = path
|
||||
self.text_viewer_content = res
|
||||
self.show_text_viewer = True
|
||||
if code_block:
|
||||
# Render code block with highlighting
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
last_idx = match.end()
|
||||
after = content[last_idx:]
|
||||
if after:
|
||||
if content.strip():
|
||||
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
|
||||
matches = list(pattern.finditer(content))
|
||||
is_nerv = theme.is_nerv_active()
|
||||
if not matches:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(after, context_id=f'disc_{i}_a')
|
||||
markdown_helper.render(content, context_id=f'disc_{i}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True)
|
||||
if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x)
|
||||
last_idx = 0
|
||||
for m_idx, match in enumerate(matches):
|
||||
before = content[last_idx:match.start()]
|
||||
if before:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
header_text = match.group(0).split("\n")[0].strip()
|
||||
path = match.group(2)
|
||||
code_block = match.group(4)
|
||||
if imgui.collapsing_header(header_text):
|
||||
if imgui.button(f"[Source]##{i}_{match.start()}"):
|
||||
res = mcp_client.read_file(path)
|
||||
if res:
|
||||
self.text_viewer_title = path
|
||||
self.text_viewer_content = res
|
||||
self.show_text_viewer = True
|
||||
if code_block:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
last_idx = match.end()
|
||||
after = content[last_idx:]
|
||||
if after:
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(after, context_id=f'disc_{i}_a')
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
if self.ui_word_wrap: imgui.pop_text_wrap_pos()
|
||||
imgui.end_child()
|
||||
else:
|
||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||
if not is_standalone:
|
||||
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
|
||||
imgui.separator()
|
||||
imgui.pop_id()
|
||||
if self._scroll_disc_to_bottom:
|
||||
@@ -2730,14 +2777,24 @@ def hello():
|
||||
imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True)
|
||||
is_nerv = theme.is_nerv_active()
|
||||
if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80))
|
||||
markdown_helper.render(self.ai_response, context_id="response")
|
||||
|
||||
segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response)
|
||||
if segments:
|
||||
self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], 9999)
|
||||
|
||||
markdown_helper.render(parsed_response, context_id="response")
|
||||
|
||||
if is_nerv: imgui.pop_style_color()
|
||||
imgui.end_child()
|
||||
|
||||
imgui.separator()
|
||||
if imgui.button("-> History"):
|
||||
if self.ai_response:
|
||||
self.disc_entries.append({"role": "AI", "content": self.ai_response, "collapsed": True, "ts": project_manager.now_ts()})
|
||||
segments, response = thinking_parser.parse_thinking_trace(self.ai_response)
|
||||
entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()}
|
||||
if segments:
|
||||
entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
|
||||
self.disc_entries.append(entry)
|
||||
if is_blinking:
|
||||
imgui.pop_style_color(2)
|
||||
if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel")
|
||||
@@ -2853,6 +2910,12 @@ def hello():
|
||||
imgui.text_colored(C_LBL, f"#{i_display}")
|
||||
imgui.same_line()
|
||||
imgui.text_colored(vec4(160, 160, 160), ts)
|
||||
|
||||
latency = entry.get("latency") or entry.get("metadata", {}).get("latency")
|
||||
if latency:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(C_SUB, f" ({latency:.2f}s)")
|
||||
|
||||
ticket_id = entry.get("mma_ticket_id")
|
||||
if ticket_id:
|
||||
imgui.same_line()
|
||||
@@ -2871,14 +2934,34 @@ def hello():
|
||||
# Optimized content rendering using _render_heavy_text logic
|
||||
idx_str = str(i)
|
||||
if kind == "request":
|
||||
usage = payload.get("usage", {})
|
||||
if usage:
|
||||
inp = usage.get("input_tokens", 0)
|
||||
imgui.text_colored(C_LBL, f" tokens in:{inp}")
|
||||
self._render_heavy_text("message", payload.get("message", ""), idx_str)
|
||||
if payload.get("system"):
|
||||
self._render_heavy_text("system", payload.get("system", ""), idx_str)
|
||||
elif kind == "response":
|
||||
r = payload.get("round", 0)
|
||||
sr = payload.get("stop_reason", "STOP")
|
||||
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}")
|
||||
self._render_heavy_text("text", payload.get("text", ""), idx_str)
|
||||
usage = payload.get("usage", {})
|
||||
usage_str = ""
|
||||
if usage:
|
||||
inp = usage.get("input_tokens", 0)
|
||||
out = usage.get("output_tokens", 0)
|
||||
cache = usage.get("cache_read_input_tokens", 0)
|
||||
usage_str = f" in:{inp} out:{out}"
|
||||
if cache:
|
||||
usage_str += f" cache:{cache}"
|
||||
imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}")
|
||||
|
||||
text_content = payload.get("text", "")
|
||||
segments, parsed_response = thinking_parser.parse_thinking_trace(text_content)
|
||||
if segments:
|
||||
self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip()))
|
||||
if parsed_response:
|
||||
self._render_heavy_text("text", parsed_response, idx_str)
|
||||
|
||||
tcs = payload.get("tool_calls", [])
|
||||
if tcs:
|
||||
self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str)
|
||||
|
||||
@@ -111,6 +111,7 @@ DEFAULT_TOOL_CATEGORIES: Dict[str, List[str]] = {
|
||||
|
||||
def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[dict[str, Any]]:
|
||||
import re
|
||||
from src import thinking_parser
|
||||
entries = []
|
||||
for raw in history_strings:
|
||||
ts = ""
|
||||
@@ -128,11 +129,30 @@ def parse_history_entries(history_strings: list[str], roles: list[str]) -> list[
|
||||
content = rest[match.end():].strip()
|
||||
else:
|
||||
content = rest
|
||||
entries.append({"role": role, "content": content, "collapsed": True, "ts": ts})
|
||||
|
||||
entry_obj = {"role": role, "content": content, "collapsed": True, "ts": ts}
|
||||
if role == "AI" and ("<thinking>" in content or "<thought>" in content or "Thinking:" in content):
|
||||
segments, parsed_content = thinking_parser.parse_thinking_trace(content)
|
||||
if segments:
|
||||
entry_obj["content"] = parsed_content
|
||||
entry_obj["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments]
|
||||
|
||||
entries.append(entry_obj)
|
||||
return entries
|
||||
|
||||
@dataclass
|
||||
@dataclass
|
||||
class ThinkingSegment:
|
||||
content: str
|
||||
marker: str # 'thinking', 'thought', or 'Thinking:'
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {"content": self.content, "marker": self.marker}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> "ThinkingSegment":
|
||||
return cls(content=data["content"], marker=data["marker"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ticket:
|
||||
id: str
|
||||
@@ -239,8 +259,6 @@ class Track:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass
|
||||
@dataclass
|
||||
class WorkerContext:
|
||||
ticket_id: str
|
||||
|
||||
@@ -33,6 +33,14 @@ def entry_to_str(entry: dict[str, Any]) -> str:
|
||||
ts = entry.get("ts", "")
|
||||
role = entry.get("role", "User")
|
||||
content = entry.get("content", "")
|
||||
|
||||
segments = entry.get("thinking_segments")
|
||||
if segments:
|
||||
for s in segments:
|
||||
marker = s.get("marker", "thinking")
|
||||
s_content = s.get("content", "")
|
||||
content = f"<{marker}>\n{s_content}\n</{marker}>\n{content}"
|
||||
|
||||
if ts:
|
||||
return f"@{ts}\n{role}:\n{content}"
|
||||
return f"{role}:\n{content}"
|
||||
|
||||
53
src/thinking_parser.py
Normal file
53
src/thinking_parser.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
from src.models import ThinkingSegment
|
||||
|
||||
def parse_thinking_trace(text: str) -> Tuple[List[ThinkingSegment], str]:
|
||||
"""
|
||||
Parses thinking segments from text and returns (segments, response_content).
|
||||
Support extraction of thinking traces from <thinking>...</thinking>, <thought>...</thought>,
|
||||
and blocks prefixed with Thinking:.
|
||||
"""
|
||||
segments = []
|
||||
|
||||
# 1. Extract <thinking> and <thought> tags
|
||||
current_text = text
|
||||
|
||||
# Combined pattern for tags
|
||||
tag_pattern = re.compile(r'<(thinking|thought)>(.*?)</\1>', re.DOTALL | re.IGNORECASE)
|
||||
|
||||
def extract_tags(txt: str) -> Tuple[List[ThinkingSegment], str]:
|
||||
found_segments = []
|
||||
|
||||
def replace_func(match):
|
||||
marker = match.group(1).lower()
|
||||
content = match.group(2).strip()
|
||||
found_segments.append(ThinkingSegment(content=content, marker=marker))
|
||||
return ""
|
||||
|
||||
remaining = tag_pattern.sub(replace_func, txt)
|
||||
return found_segments, remaining
|
||||
|
||||
tag_segments, remaining = extract_tags(current_text)
|
||||
segments.extend(tag_segments)
|
||||
|
||||
# 2. Extract Thinking: prefix
|
||||
# This usually appears at the start of a block and ends with a double newline or a response marker.
|
||||
thinking_colon_pattern = re.compile(r'(?:^|\n)Thinking:\s*(.*?)(?:\n\n|\nResponse:|\nAnswer:|$)', re.DOTALL | re.IGNORECASE)
|
||||
|
||||
def extract_colon_blocks(txt: str) -> Tuple[List[ThinkingSegment], str]:
|
||||
found_segments = []
|
||||
|
||||
def replace_func(match):
|
||||
content = match.group(1).strip()
|
||||
if content:
|
||||
found_segments.append(ThinkingSegment(content=content, marker="Thinking:"))
|
||||
return "\n\n"
|
||||
|
||||
res = thinking_colon_pattern.sub(replace_func, txt)
|
||||
return found_segments, res
|
||||
|
||||
colon_segments, final_remaining = extract_colon_blocks(remaining)
|
||||
segments.extend(colon_segments)
|
||||
|
||||
return segments, final_remaining.strip()
|
||||
BIN
temp_gui.py
Normal file
BIN
temp_gui.py
Normal file
Binary file not shown.
53
tests/test_thinking_gui.py
Normal file
53
tests/test_thinking_gui.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_render_thinking_trace_helper_exists():
|
||||
from src.gui_2 import App
|
||||
|
||||
assert hasattr(App, "_render_thinking_trace"), (
|
||||
"_render_thinking_trace helper should exist in App class"
|
||||
)
|
||||
|
||||
|
||||
def test_discussion_entry_with_thinking_segments():
|
||||
entry = {
|
||||
"role": "AI",
|
||||
"content": "Here's my response",
|
||||
"thinking_segments": [
|
||||
{"content": "Let me analyze this step by step...", "marker": "thinking"},
|
||||
{"content": "I should consider edge cases...", "marker": "thought"},
|
||||
],
|
||||
"ts": "2026-03-13T10:00:00",
|
||||
"collapsed": False,
|
||||
}
|
||||
assert "thinking_segments" in entry
|
||||
assert len(entry["thinking_segments"]) == 2
|
||||
|
||||
|
||||
def test_discussion_entry_without_thinking():
|
||||
entry = {
|
||||
"role": "User",
|
||||
"content": "Hello",
|
||||
"ts": "2026-03-13T10:00:00",
|
||||
"collapsed": False,
|
||||
}
|
||||
assert "thinking_segments" not in entry
|
||||
|
||||
|
||||
def test_thinking_segment_model_compatibility():
|
||||
from src.models import ThinkingSegment
|
||||
|
||||
segment = ThinkingSegment(content="test", marker="thinking")
|
||||
assert segment.content == "test"
|
||||
assert segment.marker == "thinking"
|
||||
d = segment.to_dict()
|
||||
assert d["content"] == "test"
|
||||
assert d["marker"] == "thinking"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_render_thinking_trace_helper_exists()
|
||||
test_discussion_entry_with_thinking_segments()
|
||||
test_discussion_entry_without_thinking()
|
||||
test_thinking_segment_model_compatibility()
|
||||
print("All GUI thinking trace tests passed!")
|
||||
94
tests/test_thinking_persistence.py
Normal file
94
tests/test_thinking_persistence.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import pytest
|
||||
import tempfile
|
||||
import os
|
||||
from pathlib import Path
|
||||
from src import project_manager
|
||||
from src.models import ThinkingSegment
|
||||
|
||||
|
||||
def test_save_and_load_history_with_thinking_segments():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
project_path = Path(tmpdir) / "test_project"
|
||||
project_path.mkdir()
|
||||
|
||||
project_file = project_path / "test_project.toml"
|
||||
project_file.write_text("[project]\nname = 'test'\n")
|
||||
|
||||
history_data = {
|
||||
"entries": [
|
||||
{
|
||||
"role": "AI",
|
||||
"content": "Here's the response",
|
||||
"thinking_segments": [
|
||||
{"content": "Let me think about this...", "marker": "thinking"}
|
||||
],
|
||||
"ts": "2026-03-13T10:00:00",
|
||||
"collapsed": False,
|
||||
},
|
||||
{
|
||||
"role": "User",
|
||||
"content": "Hello",
|
||||
"ts": "2026-03-13T09:00:00",
|
||||
"collapsed": False,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
project_manager.save_project(
|
||||
{"project": {"name": "test"}}, project_file, disc_data=history_data
|
||||
)
|
||||
|
||||
loaded = project_manager.load_history(project_file)
|
||||
|
||||
assert "entries" in loaded
|
||||
assert len(loaded["entries"]) == 2
|
||||
|
||||
ai_entry = loaded["entries"][0]
|
||||
assert ai_entry["role"] == "AI"
|
||||
assert ai_entry["content"] == "Here's the response"
|
||||
assert "thinking_segments" in ai_entry
|
||||
assert len(ai_entry["thinking_segments"]) == 1
|
||||
assert (
|
||||
ai_entry["thinking_segments"][0]["content"] == "Let me think about this..."
|
||||
)
|
||||
|
||||
user_entry = loaded["entries"][1]
|
||||
assert user_entry["role"] == "User"
|
||||
assert "thinking_segments" not in user_entry
|
||||
|
||||
|
||||
def test_entry_to_str_with_thinking():
|
||||
entry = {
|
||||
"role": "AI",
|
||||
"content": "Response text",
|
||||
"thinking_segments": [{"content": "Thinking...", "marker": "thinking"}],
|
||||
"ts": "2026-03-13T10:00:00",
|
||||
}
|
||||
result = project_manager.entry_to_str(entry)
|
||||
assert "@2026-03-13T10:00:00" in result
|
||||
assert "AI:" in result
|
||||
assert "Response text" in result
|
||||
|
||||
|
||||
def test_str_to_entry_with_thinking():
|
||||
raw = "@2026-03-13T10:00:00\nAI:\nResponse text"
|
||||
roles = ["User", "AI", "Vendor API", "System", "Reasoning"]
|
||||
result = project_manager.str_to_entry(raw, roles)
|
||||
assert result["role"] == "AI"
|
||||
assert result["content"] == "Response text"
|
||||
assert "ts" in result
|
||||
|
||||
|
||||
def test_clean_nones_removes_thinking():
|
||||
entry = {"role": "AI", "content": "Test", "thinking_segments": None, "ts": None}
|
||||
cleaned = project_manager.clean_nones(entry)
|
||||
assert "thinking_segments" not in cleaned
|
||||
assert "ts" not in cleaned
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_save_and_load_history_with_thinking_segments()
|
||||
test_entry_to_str_with_thinking()
|
||||
test_str_to_entry_with_thinking()
|
||||
test_clean_nones_removes_thinking()
|
||||
print("All project_manager thinking tests passed!")
|
||||
68
tests/test_thinking_trace.py
Normal file
68
tests/test_thinking_trace.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from src.thinking_parser import parse_thinking_trace
|
||||
|
||||
|
||||
def test_parse_xml_thinking_tag():
|
||||
raw = "<thinking>\nLet me analyze this problem step by step.\n</thinking>\nHere is the answer."
|
||||
segments, response = parse_thinking_trace(raw)
|
||||
assert len(segments) == 1
|
||||
assert segments[0].content == "Let me analyze this problem step by step."
|
||||
assert segments[0].marker == "thinking"
|
||||
assert response == "Here is the answer."
|
||||
|
||||
|
||||
def test_parse_xml_thought_tag():
|
||||
raw = "<thought>This is my reasoning process</thought>\nFinal response here."
|
||||
segments, response = parse_thinking_trace(raw)
|
||||
assert len(segments) == 1
|
||||
assert segments[0].content == "This is my reasoning process"
|
||||
assert segments[0].marker == "thought"
|
||||
assert response == "Final response here."
|
||||
|
||||
|
||||
def test_parse_text_thinking_prefix():
|
||||
raw = "Thinking:\nThis is a text-based thinking trace.\n\nNow for the actual response."
|
||||
segments, response = parse_thinking_trace(raw)
|
||||
assert len(segments) == 1
|
||||
assert segments[0].content == "This is a text-based thinking trace."
|
||||
assert segments[0].marker == "Thinking:"
|
||||
assert response == "Now for the actual response."
|
||||
|
||||
|
||||
def test_parse_no_thinking():
|
||||
raw = "This is a normal response without any thinking markers."
|
||||
segments, response = parse_thinking_trace(raw)
|
||||
assert len(segments) == 0
|
||||
assert response == raw
|
||||
|
||||
|
||||
def test_parse_empty_response():
|
||||
segments, response = parse_thinking_trace("")
|
||||
assert len(segments) == 0
|
||||
assert response == ""
|
||||
|
||||
|
||||
def test_parse_multiple_markers():
|
||||
raw = "<thinking>First thinking</thinking>\n<thought>Second thought</thought>\nResponse"
|
||||
segments, response = parse_thinking_trace(raw)
|
||||
assert len(segments) == 2
|
||||
assert segments[0].content == "First thinking"
|
||||
assert segments[1].content == "Second thought"
|
||||
|
||||
|
||||
def test_parse_thinking_with_empty_response():
|
||||
raw = "<thinking>Just thinking, no response</thinking>"
|
||||
segments, response = parse_thinking_trace(raw)
|
||||
assert len(segments) == 1
|
||||
assert segments[0].content == "Just thinking, no response"
|
||||
assert response == ""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_parse_xml_thinking_tag()
|
||||
test_parse_xml_thought_tag()
|
||||
test_parse_text_thinking_prefix()
|
||||
test_parse_no_thinking()
|
||||
test_parse_empty_response()
|
||||
test_parse_multiple_markers()
|
||||
test_parse_thinking_with_empty_response()
|
||||
print("All thinking trace tests passed!")
|
||||
Reference in New Issue
Block a user