appkit-assistant 0.15.1__tar.gz → 0.15.3__tar.gz

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.
Files changed (33) hide show
  1. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/PKG-INFO +1 -1
  2. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/pyproject.toml +1 -1
  3. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/models.py +22 -0
  4. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/openai_responses_processor.py +16 -2
  5. appkit_assistant-0.15.3/src/appkit_assistant/backend/services/thread_service.py +134 -0
  6. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/message.py +58 -2
  7. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/thread.py +9 -95
  8. appkit_assistant-0.15.3/src/appkit_assistant/logic/response_accumulator.py +219 -0
  9. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/thread_state.py +144 -345
  10. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/.gitignore +0 -0
  11. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/README.md +0 -0
  12. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/docs/assistant.png +0 -0
  13. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/mcp_auth_service.py +0 -0
  14. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/model_manager.py +0 -0
  15. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processor.py +0 -0
  16. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +0 -0
  17. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/openai_base.py +0 -0
  18. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +0 -0
  19. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
  20. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/repositories.py +0 -0
  21. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/system_prompt_cache.py +0 -0
  22. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/__init__.py +0 -0
  23. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/composer.py +0 -0
  24. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/composer_key_handler.py +0 -0
  25. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/mcp_server_dialogs.py +0 -0
  26. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/mcp_server_table.py +0 -0
  27. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/system_prompt_editor.py +0 -0
  28. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/threadlist.py +0 -0
  29. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/tools_modal.py +0 -0
  30. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/configuration.py +0 -0
  31. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/mcp_server_state.py +0 -0
  32. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/system_prompt_state.py +0 -0
  33. {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/thread_list_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appkit-assistant
3
- Version: 0.15.1
3
+ Version: 0.15.3
4
4
  Summary: Add your description here
5
5
  Project-URL: Homepage, https://github.com/jenreh/appkit
6
6
  Project-URL: Documentation, https://github.com/jenreh/appkit/tree/main/docs
@@ -7,7 +7,7 @@ dependencies = [
7
7
  "reflex>=0.8.22",
8
8
  ]
9
9
  name = "appkit-assistant"
10
- version = "0.15.1"
10
+ version = "0.15.3"
11
11
  description = "Add your description here"
12
12
  readme = "README.md"
13
13
  authors = [{ name = "Jens Rehpöhler" }]
@@ -88,6 +88,28 @@ class Message(BaseModel):
88
88
  done: bool = False
89
89
 
90
90
 
91
+ class ThinkingType(StrEnum):
92
+ REASONING = "reasoning"
93
+ TOOL_CALL = "tool_call"
94
+
95
+
96
+ class ThinkingStatus(StrEnum):
97
+ IN_PROGRESS = "in_progress"
98
+ COMPLETED = "completed"
99
+ ERROR = "error"
100
+
101
+
102
+ class Thinking(BaseModel):
103
+ type: ThinkingType
104
+ id: str # reasoning_session_id or tool_id
105
+ text: str
106
+ status: ThinkingStatus = ThinkingStatus.IN_PROGRESS
107
+ tool_name: str | None = None
108
+ parameters: str | None = None
109
+ result: str | None = None
110
+ error: str | None = None
111
+
112
+
91
113
  class AIModel(BaseModel):
92
114
  id: str
93
115
  text: str
@@ -84,8 +84,22 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
84
84
  },
85
85
  )
86
86
  except Exception as e:
87
- logger.error("Error during response processing: %s", e)
88
- # Continue to yield auth chunks if any
87
+ error_msg = str(e)
88
+ logger.error("Error during response processing: %s", error_msg)
89
+ # Only yield error chunk if NOT an auth error
90
+ # and no auth servers are pending (they'll show auth card instead)
91
+ is_auth_related = (
92
+ self._is_auth_error(error_msg) or self._pending_auth_servers
93
+ )
94
+ if not is_auth_related:
95
+ yield Chunk(
96
+ type=ChunkType.ERROR,
97
+ text=f"Ein Fehler ist aufgetreten: {error_msg}",
98
+ chunk_metadata={
99
+ "source": "responses_api",
100
+ "error_type": type(e).__name__,
101
+ },
102
+ )
89
103
 
90
104
  # After processing (or on error), yield any pending auth requirements
91
105
  logger.debug(
@@ -0,0 +1,134 @@
1
+ import logging
2
+ import uuid
3
+
4
+ from appkit_assistant.backend.model_manager import ModelManager
5
+ from appkit_assistant.backend.models import (
6
+ AssistantThread,
7
+ Message,
8
+ ThreadModel,
9
+ ThreadStatus,
10
+ )
11
+ from appkit_assistant.backend.repositories import thread_repo
12
+ from appkit_commons.database.session import get_asyncdb_session
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ThreadService:
18
+ """
19
+ Service for managing assistant threads.
20
+ Handles creation, loading, and persistence of threads.
21
+ """
22
+
23
+ def __init__(self):
24
+ self.model_manager = ModelManager()
25
+
26
+ def create_new_thread(
27
+ self, current_model: str, user_roles: list[str] | None = None
28
+ ) -> ThreadModel:
29
+ """Create a new ephemeral thread model (not persisted)."""
30
+ if user_roles is None:
31
+ user_roles = []
32
+
33
+ all_models = self.model_manager.get_all_models()
34
+
35
+ # Validate or fallback model
36
+ available_model_ids = [
37
+ m.id
38
+ for m in all_models
39
+ if not m.requires_role or m.requires_role in user_roles
40
+ ]
41
+
42
+ selected_model = current_model
43
+ if selected_model not in available_model_ids:
44
+ selected_model = self.model_manager.get_default_model()
45
+ if available_model_ids and selected_model not in available_model_ids:
46
+ selected_model = available_model_ids[0]
47
+
48
+ return ThreadModel(
49
+ thread_id=str(uuid.uuid4()),
50
+ title="Neuer Chat",
51
+ prompt="",
52
+ messages=[],
53
+ state=ThreadStatus.NEW,
54
+ ai_model=selected_model,
55
+ active=True,
56
+ )
57
+
58
+ async def load_thread(
59
+ self, thread_id: str, user_id: str | int
60
+ ) -> ThreadModel | None:
61
+ """Load a thread from the database."""
62
+ async with get_asyncdb_session() as session:
63
+ # Ensure user_id is correct type if needed
64
+ user_id_val = (
65
+ int(user_id)
66
+ if isinstance(user_id, str) and user_id.isdigit()
67
+ else user_id
68
+ )
69
+
70
+ thread_entity = await thread_repo.find_by_thread_id_and_user(
71
+ session, thread_id, user_id_val
72
+ )
73
+
74
+ if not thread_entity:
75
+ return None
76
+
77
+ return ThreadModel(
78
+ thread_id=thread_entity.thread_id,
79
+ title=thread_entity.title,
80
+ state=ThreadStatus(thread_entity.state),
81
+ ai_model=thread_entity.ai_model,
82
+ active=thread_entity.active,
83
+ messages=[Message(**m) for m in thread_entity.messages],
84
+ )
85
+
86
+ async def save_thread(self, thread: ThreadModel, user_id: str | int) -> None:
87
+ """Persist or update a thread in the database."""
88
+ if not user_id:
89
+ logger.warning("Cannot save thread: No user ID provided")
90
+ return
91
+
92
+ try:
93
+ messages_dict = [m.model_dump() for m in thread.messages]
94
+ user_id_val = (
95
+ int(user_id)
96
+ if isinstance(user_id, str) and user_id.isdigit()
97
+ else user_id
98
+ )
99
+
100
+ async with get_asyncdb_session() as session:
101
+ existing = await thread_repo.find_by_thread_id_and_user(
102
+ session, thread.thread_id, user_id_val
103
+ )
104
+
105
+ state_value = (
106
+ thread.state.value
107
+ if hasattr(thread.state, "value")
108
+ else thread.state
109
+ )
110
+
111
+ if existing:
112
+ existing.title = thread.title
113
+ existing.state = state_value
114
+ existing.ai_model = thread.ai_model
115
+ existing.active = thread.active
116
+ existing.messages = messages_dict
117
+ # updated_at handled by DB/SQLModel defaults,
118
+ # explicit save triggers it
119
+ await thread_repo.save(session, existing)
120
+ else:
121
+ new_thread = AssistantThread(
122
+ thread_id=thread.thread_id,
123
+ user_id=user_id_val,
124
+ title=thread.title,
125
+ state=state_value,
126
+ ai_model=thread.ai_model,
127
+ active=thread.active,
128
+ messages=messages_dict,
129
+ )
130
+ await thread_repo.save(session, new_thread)
131
+
132
+ logger.debug("Saved thread to DB: %s", thread.thread_id)
133
+ except Exception as e:
134
+ logger.exception("Error saving thread %s: %s", thread.thread_id, e)
@@ -4,11 +4,11 @@ import appkit_mantine as mn
4
4
  from appkit_assistant.backend.models import (
5
5
  Message,
6
6
  MessageType,
7
- )
8
- from appkit_assistant.state.thread_state import (
9
7
  Thinking,
10
8
  ThinkingStatus,
11
9
  ThinkingType,
10
+ )
11
+ from appkit_assistant.state.thread_state import (
12
12
  ThreadState,
13
13
  )
14
14
  from appkit_ui.components.collabsible import collabsible
@@ -229,6 +229,54 @@ class MessageComponent:
229
229
  style=message_styles,
230
230
  )
231
231
 
232
+ @staticmethod
233
+ def error_message(message: str) -> rx.Component:
234
+ return rx.hstack(
235
+ rx.avatar(
236
+ fallback="!",
237
+ size="3",
238
+ variant="soft",
239
+ radius="full",
240
+ margin_top="16px",
241
+ color_scheme="red",
242
+ ),
243
+ rx.callout(
244
+ message,
245
+ icon="triangle-alert",
246
+ color_scheme="red",
247
+ max_width="90%",
248
+ size="1",
249
+ padding="0.5em",
250
+ border_radius="9px",
251
+ margin_top="18px",
252
+ ),
253
+ style=message_styles,
254
+ )
255
+
256
+ @staticmethod
257
+ def system_message(message: str) -> rx.Component:
258
+ return rx.hstack(
259
+ rx.avatar(
260
+ fallback="⚙",
261
+ size="3",
262
+ variant="soft",
263
+ radius="full",
264
+ margin_top="16px",
265
+ color_scheme="gray",
266
+ ),
267
+ rx.callout(
268
+ message,
269
+ icon="info",
270
+ color_scheme="gray",
271
+ max_width="90%",
272
+ size="1",
273
+ padding="0.5em",
274
+ border_radius="9px",
275
+ margin_top="18px",
276
+ ),
277
+ style=message_styles,
278
+ )
279
+
232
280
  @staticmethod
233
281
  def render_message(
234
282
  message: Message,
@@ -245,6 +293,14 @@ class MessageComponent:
245
293
  MessageType.ASSISTANT,
246
294
  MessageComponent.assistant_message(message),
247
295
  ),
296
+ (
297
+ MessageType.ERROR,
298
+ MessageComponent.error_message(message.text),
299
+ ),
300
+ (
301
+ MessageType.SYSTEM,
302
+ MessageComponent.system_message(message.text),
303
+ ),
248
304
  MessageComponent.info_message(message.text),
249
305
  )
250
306
  )
@@ -155,101 +155,15 @@ class Assistant:
155
155
  # ThreadState.set_suggestions(suggestions)
156
156
 
157
157
  return rx.flex(
158
- # Hidden element with user_id for OAuth validation
159
- rx.el.input(
160
- id="mcp-oauth-user-id",
161
- type="hidden",
162
- value=ThreadState.current_user_id,
163
- ),
164
- # Hidden button for OAuth callback - triggered by storage event
165
- rx.el.button(
166
- id="mcp-oauth-success-trigger",
167
- style={"display": "none"},
168
- on_click=lambda: ThreadState.handle_mcp_oauth_success_from_js(),
169
- ),
170
- # OAuth listener for localStorage changes (cross-window)
171
- rx.script(
172
- """
173
- (function() {
174
- // Prevent double processing
175
- if (window._mcpOAuthListenerInstalled) {
176
- console.log('[OAuth] Listener already installed, skipping');
177
- return;
178
- }
179
- window._mcpOAuthListenerInstalled = true;
180
- var processing = false;
181
-
182
- function getCurrentUserId() {
183
- var el = document.getElementById('mcp-oauth-user-id');
184
- return el ? el.value : '';
185
- }
186
- function processOAuthResult(data) {
187
- if (processing) {
188
- console.log('[OAuth] Already processing, skip');
189
- return false;
190
- }
191
- processing = true;
192
-
193
- var currentUserId = getCurrentUserId();
194
- console.log('[OAuth] Processing, userId:', data.userId,
195
- 'current:', currentUserId);
196
- // Security: only process if user_id matches (or not set)
197
- if (data.userId && currentUserId &&
198
- String(data.userId) !== String(currentUserId)) {
199
- console.log('[OAuth] Ignoring - user mismatch');
200
- processing = false;
201
- return false;
202
- }
203
- window._mcpOAuthData = data;
204
- var btn = document.getElementById(
205
- 'mcp-oauth-success-trigger'
206
- );
207
- if (btn) {
208
- console.log('[OAuth] Clicking trigger button');
209
- btn.click();
210
- }
211
- // Reset after short delay to allow for page navigation
212
- setTimeout(function() { processing = false; }, 5000);
213
- return true;
214
- }
215
- function checkLocalStorage() {
216
- if (processing) return false;
217
- var stored = localStorage.getItem('mcp-oauth-result');
218
- if (stored) {
219
- console.log('[OAuth] Found in localStorage');
220
- try {
221
- var data = JSON.parse(stored);
222
- if (data.type === 'mcp-oauth-success') {
223
- localStorage.removeItem('mcp-oauth-result');
224
- return processOAuthResult(data);
225
- }
226
- } catch(e) { console.error('[OAuth] Parse error:', e); }
227
- }
228
- return false;
229
- }
230
- console.log('[OAuth] Installing listeners');
231
- window.addEventListener('storage', function(event) {
232
- if (event.key === 'mcp-oauth-result') {
233
- checkLocalStorage();
234
- }
235
- });
236
- window.addEventListener('focus', function() {
237
- checkLocalStorage();
238
- });
239
- document.addEventListener('visibilitychange', function() {
240
- if (!document.hidden) checkLocalStorage();
241
- });
242
- var intervalId = setInterval(function() {
243
- if (checkLocalStorage()) clearInterval(intervalId);
244
- }, 2000);
245
- checkLocalStorage();
246
- window.addEventListener('message', function(event) {
247
- if (event.data && event.data.type === 'mcp-oauth-success') {
248
- processOAuthResult(event.data);
249
- }
250
- });
251
- })();
252
- """
158
+ # OAuth result handler - triggers when popup sets localStorage
159
+ # rx.LocalStorage(sync=True) auto-syncs, rx.cond re-renders on change
160
+ rx.cond(
161
+ ThreadState.oauth_result != "",
162
+ rx.box(
163
+ on_mount=ThreadState.process_oauth_result,
164
+ style={"display": "none"},
165
+ ),
166
+ rx.fragment(),
253
167
  ),
254
168
  rx.cond(
255
169
  ThreadState.messages,
@@ -0,0 +1,219 @@
1
+ import logging
2
+ import uuid
3
+ from typing import Any
4
+
5
+ from appkit_assistant.backend.models import (
6
+ Chunk,
7
+ ChunkType,
8
+ Message,
9
+ MessageType,
10
+ Thinking,
11
+ ThinkingStatus,
12
+ ThinkingType,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ResponseAccumulator:
19
+ """
20
+ Accumulates chunks from streaming response into structured data
21
+ (Messages, Thinking items, etc.) for UI display.
22
+ """
23
+
24
+ def __init__(self):
25
+ self.current_reasoning_session: str = ""
26
+ self.current_tool_session: str = ""
27
+ self.thinking_items: list[Thinking] = []
28
+ self.image_chunks: list[Chunk] = []
29
+ self.messages: list[Message] = []
30
+ self.show_thinking: bool = False
31
+ self.current_activity: str = ""
32
+
33
+ # State for auth/errors
34
+ self.pending_auth_server_id: str = ""
35
+ self.pending_auth_server_name: str = ""
36
+ self.pending_auth_url: str = ""
37
+ self.auth_required: bool = False
38
+ self.error: str | None = None
39
+
40
+ def attach_messages_ref(self, messages: list[Message]) -> None:
41
+ """Attach a reference to the mutable messages list from state."""
42
+ self.messages = messages
43
+
44
+ def process_chunk(self, chunk: Chunk) -> None:
45
+ """Process a single chunk and update internal state."""
46
+ if chunk.type == ChunkType.TEXT:
47
+ if self.messages and self.messages[-1].type == MessageType.ASSISTANT:
48
+ self.messages[-1].text += chunk.text
49
+
50
+ elif chunk.type in (ChunkType.THINKING, ChunkType.THINKING_RESULT):
51
+ self._handle_reasoning_chunk(chunk)
52
+
53
+ elif chunk.type in (
54
+ ChunkType.TOOL_CALL,
55
+ ChunkType.TOOL_RESULT,
56
+ ChunkType.ACTION,
57
+ ):
58
+ self._handle_tool_chunk(chunk)
59
+
60
+ elif chunk.type in (ChunkType.IMAGE, ChunkType.IMAGE_PARTIAL):
61
+ self.image_chunks.append(chunk)
62
+
63
+ elif chunk.type == ChunkType.COMPLETION:
64
+ self.show_thinking = False
65
+
66
+ elif chunk.type == ChunkType.AUTH_REQUIRED:
67
+ self._handle_auth_required_chunk(chunk)
68
+
69
+ elif chunk.type == ChunkType.ERROR:
70
+ # We append it to the message text if it's not a hard error,
71
+ # or creates a new message?
72
+ # Existing logic was appending a new message.
73
+ self.messages.append(Message(text=chunk.text, type=MessageType.ERROR))
74
+ self.error = chunk.text
75
+
76
+ else:
77
+ logger.warning("Unhandled chunk type: %s", chunk.type)
78
+
79
+ def _get_or_create_tool_session(self, chunk: Chunk) -> str:
80
+ tool_id = chunk.chunk_metadata.get("tool_id")
81
+ if tool_id:
82
+ self.current_tool_session = tool_id
83
+ return tool_id
84
+
85
+ if chunk.type == ChunkType.TOOL_CALL:
86
+ # Count existing tool calls
87
+ tool_count = sum(
88
+ 1 for i in self.thinking_items if i.type == ThinkingType.TOOL_CALL
89
+ )
90
+ self.current_tool_session = f"tool_{tool_count}"
91
+ return self.current_tool_session
92
+
93
+ if self.current_tool_session:
94
+ return self.current_tool_session
95
+
96
+ # Fallback
97
+ tool_count = sum(
98
+ 1 for i in self.thinking_items if i.type == ThinkingType.TOOL_CALL
99
+ )
100
+ self.current_tool_session = f"tool_{tool_count}"
101
+ return self.current_tool_session
102
+
103
+ def _handle_reasoning_chunk(self, chunk: Chunk) -> None:
104
+ if chunk.type == ChunkType.THINKING:
105
+ self.show_thinking = True
106
+ self.current_activity = "Denke nach..."
107
+
108
+ reasoning_session = self._get_or_create_reasoning_session(chunk)
109
+
110
+ status = ThinkingStatus.IN_PROGRESS
111
+ text = ""
112
+ if chunk.type == ChunkType.THINKING:
113
+ text = chunk.text
114
+ elif chunk.type == ChunkType.THINKING_RESULT:
115
+ status = ThinkingStatus.COMPLETED
116
+
117
+ item = self._get_or_create_thinking_item(
118
+ reasoning_session, ThinkingType.REASONING, text=text, status=status
119
+ )
120
+
121
+ if chunk.type == ChunkType.THINKING:
122
+ if item.text and item.text != text:
123
+ item.text += f"\n{chunk.text}"
124
+ elif chunk.type == ChunkType.THINKING_RESULT:
125
+ item.status = ThinkingStatus.COMPLETED
126
+ if chunk.text:
127
+ item.text += f" {chunk.text}"
128
+
129
+ def _get_or_create_reasoning_session(self, chunk: Chunk) -> str:
130
+ reasoning_session = chunk.chunk_metadata.get("reasoning_session")
131
+ if reasoning_session:
132
+ return reasoning_session
133
+
134
+ last_item = self.thinking_items[-1] if self.thinking_items else None
135
+
136
+ should_create_new = (
137
+ not self.current_reasoning_session
138
+ or (last_item and last_item.type == ThinkingType.TOOL_CALL)
139
+ or (
140
+ last_item
141
+ and last_item.type == ThinkingType.REASONING
142
+ and last_item.status == ThinkingStatus.COMPLETED
143
+ )
144
+ )
145
+
146
+ if should_create_new:
147
+ self.current_reasoning_session = f"reasoning_{uuid.uuid4().hex[:8]}"
148
+
149
+ return self.current_reasoning_session
150
+
151
+ def _handle_tool_chunk(self, chunk: Chunk) -> None:
152
+ tool_id = self._get_or_create_tool_session(chunk)
153
+
154
+ tool_name = chunk.chunk_metadata.get("tool_name", "Unknown")
155
+ if chunk.type == ChunkType.TOOL_CALL:
156
+ self.current_activity = f"Nutze Werkzeug: {tool_name}..."
157
+
158
+ status = ThinkingStatus.IN_PROGRESS
159
+ text = ""
160
+ parameters = None
161
+ result = None
162
+ error = None
163
+
164
+ if chunk.type == ChunkType.TOOL_CALL:
165
+ parameters = chunk.chunk_metadata.get("parameters", chunk.text)
166
+ text = chunk.chunk_metadata.get("description", "")
167
+ elif chunk.type == ChunkType.TOOL_RESULT:
168
+ is_error = (
169
+ "error" in chunk.text.lower()
170
+ or "failed" in chunk.text.lower()
171
+ or chunk.chunk_metadata.get("error")
172
+ )
173
+ status = ThinkingStatus.ERROR if is_error else ThinkingStatus.COMPLETED
174
+ result = chunk.text
175
+ if is_error:
176
+ error = chunk.text
177
+ else:
178
+ text = chunk.text
179
+
180
+ item = self._get_or_create_thinking_item(
181
+ tool_id,
182
+ ThinkingType.TOOL_CALL,
183
+ text=text,
184
+ status=status,
185
+ tool_name=tool_name,
186
+ parameters=parameters,
187
+ result=result,
188
+ error=error,
189
+ )
190
+
191
+ if chunk.type == ChunkType.TOOL_CALL:
192
+ item.parameters = parameters
193
+ item.text = text
194
+ if not item.tool_name or item.tool_name == "Unknown":
195
+ item.tool_name = tool_name
196
+ item.status = ThinkingStatus.IN_PROGRESS
197
+ elif chunk.type == ChunkType.TOOL_RESULT:
198
+ item.status = status
199
+ item.result = result
200
+ item.error = error
201
+ elif chunk.type == ChunkType.ACTION:
202
+ item.text += f"\n---\nAktion: {chunk.text}"
203
+
204
+ def _get_or_create_thinking_item(
205
+ self, item_id: str, thinking_type: ThinkingType, **kwargs: Any
206
+ ) -> Thinking:
207
+ for item in self.thinking_items:
208
+ if item.type == thinking_type and item.id == item_id:
209
+ return item
210
+
211
+ new_item = Thinking(type=thinking_type, id=item_id, **kwargs)
212
+ self.thinking_items.append(new_item)
213
+ return new_item
214
+
215
+ def _handle_auth_required_chunk(self, chunk: Chunk) -> None:
216
+ self.pending_auth_server_id = chunk.chunk_metadata.get("server_id", "")
217
+ self.pending_auth_server_name = chunk.chunk_metadata.get("server_name", "")
218
+ self.pending_auth_url = chunk.chunk_metadata.get("auth_url", "")
219
+ self.auth_required = True