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.
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/PKG-INFO +1 -1
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/pyproject.toml +1 -1
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/models.py +22 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/openai_responses_processor.py +16 -2
- appkit_assistant-0.15.3/src/appkit_assistant/backend/services/thread_service.py +134 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/message.py +58 -2
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/thread.py +9 -95
- appkit_assistant-0.15.3/src/appkit_assistant/logic/response_accumulator.py +219 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/thread_state.py +144 -345
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/.gitignore +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/README.md +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/docs/assistant.png +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/mcp_auth_service.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/model_manager.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processor.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/openai_base.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/repositories.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/backend/system_prompt_cache.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/__init__.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/composer.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/composer_key_handler.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/mcp_server_dialogs.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/mcp_server_table.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/system_prompt_editor.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/threadlist.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/tools_modal.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/configuration.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/mcp_server_state.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/system_prompt_state.py +0 -0
- {appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/state/thread_list_state.py +0 -0
|
@@ -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
|
-
|
|
88
|
-
|
|
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)
|
{appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/message.py
RENAMED
|
@@ -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
|
)
|
{appkit_assistant-0.15.1 → appkit_assistant-0.15.3}/src/appkit_assistant/components/thread.py
RENAMED
|
@@ -155,101 +155,15 @@ class Assistant:
|
|
|
155
155
|
# ThreadState.set_suggestions(suggestions)
|
|
156
156
|
|
|
157
157
|
return rx.flex(
|
|
158
|
-
#
|
|
159
|
-
rx.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|