appkit-assistant 0.8.0__py3-none-any.whl → 0.10.0__py3-none-any.whl
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/backend/models.py +93 -2
- appkit_assistant/backend/processors/openai_responses_processor.py +16 -11
- appkit_assistant/backend/repositories.py +228 -1
- appkit_assistant/backend/system_prompt_cache.py +161 -0
- appkit_assistant/components/__init__.py +2 -4
- appkit_assistant/components/mcp_server_dialogs.py +7 -2
- appkit_assistant/components/message.py +3 -3
- appkit_assistant/components/system_prompt_editor.py +78 -0
- appkit_assistant/components/thread.py +8 -16
- appkit_assistant/components/threadlist.py +42 -29
- appkit_assistant/components/tools_modal.py +1 -1
- appkit_assistant/configuration.py +1 -0
- appkit_assistant/state/system_prompt_state.py +179 -0
- appkit_assistant/state/thread_list_state.py +271 -0
- appkit_assistant/state/thread_state.py +525 -608
- {appkit_assistant-0.8.0.dist-info → appkit_assistant-0.10.0.dist-info}/METADATA +2 -2
- appkit_assistant-0.10.0.dist-info/RECORD +31 -0
- appkit_assistant-0.8.0.dist-info/RECORD +0 -27
- {appkit_assistant-0.8.0.dist-info → appkit_assistant-0.10.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import reflex as rx
|
|
2
|
+
|
|
3
|
+
import appkit_mantine as mn
|
|
4
|
+
from appkit_assistant.state.system_prompt_state import SystemPromptState
|
|
5
|
+
from appkit_ui.components.dialogs import delete_dialog
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def system_prompt_editor() -> rx.Component:
|
|
9
|
+
"""Admin-UI für das System Prompt Versioning mit appkit-mantine & appkit-ui.
|
|
10
|
+
|
|
11
|
+
Uses a hybrid approach for the textarea:
|
|
12
|
+
- default_value + on_change prevents cursor jumping during typing
|
|
13
|
+
- key prop forces re-render when selecting a different version
|
|
14
|
+
- This gives us both smooth editing AND the ability to update from select
|
|
15
|
+
"""
|
|
16
|
+
return rx.vstack(
|
|
17
|
+
mn.markdown_preview(
|
|
18
|
+
source=(
|
|
19
|
+
"""
|
|
20
|
+
Der System-Prompt legt fest, wie sich der Assistent verhält. Bitte stellen Sie sicher,
|
|
21
|
+
dass der Platzhalter `{mcp_prompts}` immer im Text enthalten ist. Dieser Platzhalter ist
|
|
22
|
+
notwendig, damit das System im Hintergrund die benötigten Funktionen und Werkzeuge
|
|
23
|
+
automatisch einfügen kann."""
|
|
24
|
+
),
|
|
25
|
+
width="100%",
|
|
26
|
+
),
|
|
27
|
+
mn.textarea(
|
|
28
|
+
placeholder="System-Prompt hier eingeben (max. 10.000 Zeichen)...",
|
|
29
|
+
description=f"{SystemPromptState.char_count} / 10.000 Zeichen",
|
|
30
|
+
default_value=SystemPromptState.current_prompt,
|
|
31
|
+
on_change=SystemPromptState.set_current_prompt,
|
|
32
|
+
error=SystemPromptState.error_message,
|
|
33
|
+
rows=21,
|
|
34
|
+
variant="filled",
|
|
35
|
+
width="100%",
|
|
36
|
+
key=SystemPromptState.textarea_key,
|
|
37
|
+
),
|
|
38
|
+
rx.hstack(
|
|
39
|
+
mn.select(
|
|
40
|
+
placeholder="Aktuell",
|
|
41
|
+
data=SystemPromptState.versions,
|
|
42
|
+
value=SystemPromptState.selected_version_str,
|
|
43
|
+
on_change=SystemPromptState.set_selected_version,
|
|
44
|
+
clearable=False,
|
|
45
|
+
searchable=False,
|
|
46
|
+
width="280px",
|
|
47
|
+
),
|
|
48
|
+
delete_dialog(
|
|
49
|
+
title="Version endgültig löschen?",
|
|
50
|
+
content="die ausgewählte Version",
|
|
51
|
+
on_click=SystemPromptState.delete_version,
|
|
52
|
+
icon_button=True,
|
|
53
|
+
class_name="dialog",
|
|
54
|
+
variant="outline",
|
|
55
|
+
color_scheme="red",
|
|
56
|
+
disabled=SystemPromptState.is_loading
|
|
57
|
+
| (SystemPromptState.selected_version_id == 0),
|
|
58
|
+
),
|
|
59
|
+
rx.spacer(),
|
|
60
|
+
rx.button(
|
|
61
|
+
"Neue Version speichern",
|
|
62
|
+
on_click=SystemPromptState.save_current,
|
|
63
|
+
disabled=SystemPromptState.is_loading
|
|
64
|
+
| (
|
|
65
|
+
SystemPromptState.current_prompt
|
|
66
|
+
== SystemPromptState.last_saved_prompt
|
|
67
|
+
),
|
|
68
|
+
loading=SystemPromptState.is_loading,
|
|
69
|
+
),
|
|
70
|
+
align="center",
|
|
71
|
+
width="100%",
|
|
72
|
+
spacing="4",
|
|
73
|
+
),
|
|
74
|
+
width="100%",
|
|
75
|
+
max_width="960px",
|
|
76
|
+
padding="6",
|
|
77
|
+
spacing="5",
|
|
78
|
+
)
|
|
@@ -4,15 +4,11 @@ from collections.abc import Callable
|
|
|
4
4
|
import reflex as rx
|
|
5
5
|
|
|
6
6
|
import appkit_mantine as mn
|
|
7
|
+
from appkit_assistant.backend.models import Message, MessageType
|
|
7
8
|
from appkit_assistant.components import composer
|
|
8
9
|
from appkit_assistant.components.message import MessageComponent
|
|
9
10
|
from appkit_assistant.components.threadlist import ThreadList
|
|
10
|
-
from appkit_assistant.state.thread_state import
|
|
11
|
-
Message,
|
|
12
|
-
MessageType,
|
|
13
|
-
ThreadListState,
|
|
14
|
-
ThreadState,
|
|
15
|
-
)
|
|
11
|
+
from appkit_assistant.state.thread_state import ThreadState
|
|
16
12
|
|
|
17
13
|
logger = logging.getLogger(__name__)
|
|
18
14
|
|
|
@@ -55,7 +51,7 @@ class Assistant:
|
|
|
55
51
|
lambda suggestion: Assistant.suggestion(
|
|
56
52
|
prompt=suggestion.prompt,
|
|
57
53
|
icon=suggestion.icon,
|
|
58
|
-
update_prompt=ThreadState.
|
|
54
|
+
update_prompt=ThreadState.set_prompt,
|
|
59
55
|
),
|
|
60
56
|
),
|
|
61
57
|
spacing="4",
|
|
@@ -122,7 +118,7 @@ class Assistant:
|
|
|
122
118
|
),
|
|
123
119
|
rx.hstack(
|
|
124
120
|
composer.tools(
|
|
125
|
-
show=with_tools and ThreadState.
|
|
121
|
+
show=with_tools and ThreadState.selected_model_supports_tools
|
|
126
122
|
),
|
|
127
123
|
composer.add_attachment(show=with_attachments),
|
|
128
124
|
composer.clear(show=with_clear),
|
|
@@ -156,9 +152,6 @@ class Assistant:
|
|
|
156
152
|
# if suggestions is not None:
|
|
157
153
|
# ThreadState.set_suggestions(suggestions)
|
|
158
154
|
|
|
159
|
-
if with_thread_list:
|
|
160
|
-
ThreadState.with_thread_list = with_thread_list
|
|
161
|
-
|
|
162
155
|
return rx.flex(
|
|
163
156
|
rx.cond(
|
|
164
157
|
ThreadState.messages,
|
|
@@ -207,7 +200,10 @@ class Assistant:
|
|
|
207
200
|
spacing="0",
|
|
208
201
|
flex_shrink=0,
|
|
209
202
|
z_index=1000,
|
|
210
|
-
on_mount=
|
|
203
|
+
on_mount=[
|
|
204
|
+
ThreadState.set_with_thread_list(with_thread_list),
|
|
205
|
+
ThreadState.load_mcp_servers,
|
|
206
|
+
],
|
|
211
207
|
),
|
|
212
208
|
**props,
|
|
213
209
|
)
|
|
@@ -216,12 +212,8 @@ class Assistant:
|
|
|
216
212
|
def thread_list(
|
|
217
213
|
*items,
|
|
218
214
|
with_footer: bool = False,
|
|
219
|
-
default_model: str | None = None,
|
|
220
215
|
**props,
|
|
221
216
|
) -> rx.Component:
|
|
222
|
-
if default_model:
|
|
223
|
-
ThreadListState.default_model = default_model
|
|
224
|
-
|
|
225
217
|
return rx.flex(
|
|
226
218
|
rx.flex(
|
|
227
219
|
ThreadList.header(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import reflex as rx
|
|
2
2
|
|
|
3
|
-
from appkit_assistant.
|
|
3
|
+
from appkit_assistant.backend.models import ThreadModel
|
|
4
|
+
from appkit_assistant.state.thread_list_state import ThreadListState
|
|
5
|
+
from appkit_assistant.state.thread_state import ThreadState
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class ThreadList:
|
|
@@ -13,7 +15,7 @@ class ThreadList:
|
|
|
13
15
|
rx.text(title),
|
|
14
16
|
size="2",
|
|
15
17
|
margin_right="28px",
|
|
16
|
-
on_click=
|
|
18
|
+
on_click=ThreadState.new_thread(),
|
|
17
19
|
width="95%",
|
|
18
20
|
),
|
|
19
21
|
content="Neuen Chat starten",
|
|
@@ -46,24 +48,28 @@ class ThreadList:
|
|
|
46
48
|
min_width="0",
|
|
47
49
|
title=thread.title,
|
|
48
50
|
),
|
|
49
|
-
rx.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
rx.cond(
|
|
52
|
+
ThreadListState.loading_thread_id == thread.thread_id,
|
|
53
|
+
rx.spinner(size="1", margin_left="6px", margin_right="6px"),
|
|
54
|
+
rx.tooltip(
|
|
55
|
+
rx.button(
|
|
56
|
+
rx.icon(
|
|
57
|
+
"trash",
|
|
58
|
+
size=13,
|
|
59
|
+
stroke_width=1.5,
|
|
60
|
+
),
|
|
61
|
+
variant="ghost",
|
|
62
|
+
size="1",
|
|
63
|
+
margin_left="0px",
|
|
64
|
+
margin_right="0px",
|
|
65
|
+
color_scheme="gray",
|
|
66
|
+
on_click=ThreadListState.delete_thread(thread.thread_id),
|
|
55
67
|
),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
margin_left="0px",
|
|
59
|
-
margin_right="0px",
|
|
60
|
-
color_scheme="gray",
|
|
61
|
-
on_click=ThreadListState.delete_thread(thread.thread_id),
|
|
68
|
+
content="Chat löschen",
|
|
69
|
+
flex_shrink=0,
|
|
62
70
|
),
|
|
63
|
-
content="Chat löschen",
|
|
64
|
-
flex_shrink=0,
|
|
65
71
|
),
|
|
66
|
-
on_click=
|
|
72
|
+
on_click=ThreadState.load_thread(thread.thread_id),
|
|
67
73
|
flex_direction=["row"],
|
|
68
74
|
margin_right="10px",
|
|
69
75
|
margin_bottom="8px",
|
|
@@ -113,18 +119,25 @@ class ThreadList:
|
|
|
113
119
|
ThreadListState.threads,
|
|
114
120
|
ThreadList.thread_list_item,
|
|
115
121
|
),
|
|
116
|
-
rx.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
rx.cond(
|
|
123
|
+
ThreadListState.loading,
|
|
124
|
+
rx.vstack(
|
|
125
|
+
rx.skeleton(height="34px", width="210px"),
|
|
126
|
+
rx.skeleton(height="34px", width="210px"),
|
|
127
|
+
rx.skeleton(height="34px", width="210px"),
|
|
128
|
+
spacing="2",
|
|
129
|
+
),
|
|
130
|
+
rx.text(
|
|
131
|
+
"Noch keine Chats vorhanden. "
|
|
132
|
+
"Klicke auf 'Neuer Chat', um zu beginnen.",
|
|
133
|
+
size="2",
|
|
134
|
+
flex_grow="1",
|
|
135
|
+
min_width="0",
|
|
136
|
+
margin_right="10px",
|
|
137
|
+
margin_bottom="8px",
|
|
138
|
+
padding="6px",
|
|
139
|
+
align="center",
|
|
140
|
+
),
|
|
128
141
|
),
|
|
129
142
|
),
|
|
130
143
|
scrollbars="vertical",
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from typing import Any, Final
|
|
4
|
+
|
|
5
|
+
import reflex as rx
|
|
6
|
+
from reflex.state import State
|
|
7
|
+
|
|
8
|
+
from appkit_assistant.backend.repositories import SystemPromptRepository
|
|
9
|
+
from appkit_assistant.backend.system_prompt_cache import invalidate_prompt_cache
|
|
10
|
+
from appkit_user.authentication.states import UserSession
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
MAX_PROMPT_LENGTH: Final[int] = 10000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SystemPromptState(State):
|
|
18
|
+
"""State für System Prompt Editing und Versionierung."""
|
|
19
|
+
|
|
20
|
+
current_prompt: str = ""
|
|
21
|
+
last_saved_prompt: str = ""
|
|
22
|
+
versions: list[dict[str, str | int]] = []
|
|
23
|
+
prompt_map: dict[str, str] = {}
|
|
24
|
+
selected_version_id: int = 0
|
|
25
|
+
is_loading: bool = False
|
|
26
|
+
error_message: str = ""
|
|
27
|
+
char_count: int = 0
|
|
28
|
+
textarea_key: int = 0
|
|
29
|
+
|
|
30
|
+
async def load_versions(self) -> None:
|
|
31
|
+
"""Alle System Prompt Versionen laden."""
|
|
32
|
+
self.is_loading = True
|
|
33
|
+
self.error_message = ""
|
|
34
|
+
try:
|
|
35
|
+
prompts = await SystemPromptRepository.get_all()
|
|
36
|
+
self.versions = [
|
|
37
|
+
{
|
|
38
|
+
"value": str(p.version),
|
|
39
|
+
"label": (
|
|
40
|
+
f"Version {p.version} - "
|
|
41
|
+
f"{p.created_at.strftime('%d.%m.%Y %H:%M')}"
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
for p in prompts
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
self.prompt_map = {str(p.version): p.prompt for p in prompts}
|
|
48
|
+
|
|
49
|
+
if prompts:
|
|
50
|
+
latest = prompts[0]
|
|
51
|
+
self.selected_version_id = latest.version
|
|
52
|
+
|
|
53
|
+
if not self.current_prompt:
|
|
54
|
+
self.current_prompt = latest.prompt
|
|
55
|
+
self.last_saved_prompt = latest.prompt
|
|
56
|
+
else:
|
|
57
|
+
self.last_saved_prompt = latest.prompt
|
|
58
|
+
else:
|
|
59
|
+
self.selected_version_id = 0
|
|
60
|
+
if not self.current_prompt:
|
|
61
|
+
self.current_prompt = ""
|
|
62
|
+
self.last_saved_prompt = self.current_prompt
|
|
63
|
+
|
|
64
|
+
self.char_count = len(self.current_prompt)
|
|
65
|
+
# Force textarea to re-render with loaded content
|
|
66
|
+
self.textarea_key += 1
|
|
67
|
+
|
|
68
|
+
logger.info("Loaded %s system prompt versions", len(self.versions))
|
|
69
|
+
except Exception as exc:
|
|
70
|
+
self.error_message = f"Fehler beim Laden: {exc!s}"
|
|
71
|
+
logger.exception("Failed to load system prompt versions")
|
|
72
|
+
finally:
|
|
73
|
+
self.is_loading = False
|
|
74
|
+
|
|
75
|
+
async def save_current(self) -> AsyncGenerator[Any, Any]:
|
|
76
|
+
if self.current_prompt == self.last_saved_prompt:
|
|
77
|
+
yield rx.toast.info("Es wurden keine Änderungen erkannt.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if not self.current_prompt.strip():
|
|
81
|
+
self.error_message = "Prompt darf nicht leer sein."
|
|
82
|
+
yield rx.toast.error("Prompt darf nicht leer sein.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if len(self.current_prompt) > MAX_PROMPT_LENGTH:
|
|
86
|
+
self.error_message = "Prompt darf maximal 20.000 Zeichen enthalten."
|
|
87
|
+
yield rx.toast.error("Prompt ist zu lang (max. 20.000 Zeichen).")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
self.is_loading = True
|
|
91
|
+
self.error_message = ""
|
|
92
|
+
try:
|
|
93
|
+
user_session: UserSession = await self.get_state(UserSession)
|
|
94
|
+
user_id = user_session.user_id
|
|
95
|
+
|
|
96
|
+
await SystemPromptRepository.create(
|
|
97
|
+
prompt=self.current_prompt,
|
|
98
|
+
user_id=user_id,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.last_saved_prompt = self.current_prompt
|
|
102
|
+
|
|
103
|
+
# Invalidate cache to force reload of new prompt version
|
|
104
|
+
await invalidate_prompt_cache()
|
|
105
|
+
logger.info("System prompt cache invalidated after save")
|
|
106
|
+
|
|
107
|
+
await self.load_versions()
|
|
108
|
+
|
|
109
|
+
yield rx.toast.success("Neue Version erfolgreich gespeichert.")
|
|
110
|
+
logger.info("Saved new system prompt version by user %s", user_id)
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
self.error_message = f"Fehler beim Speichern: {exc!s}"
|
|
113
|
+
logger.exception("Failed to save system prompt")
|
|
114
|
+
yield rx.toast.error(f"Fehler: {exc!s}")
|
|
115
|
+
finally:
|
|
116
|
+
self.is_loading = False
|
|
117
|
+
|
|
118
|
+
async def delete_version(self) -> AsyncGenerator[Any, Any]:
|
|
119
|
+
if not self.selected_version_id:
|
|
120
|
+
self.error_message = "Keine Version ausgewählt."
|
|
121
|
+
yield rx.toast.error("Bitte zuerst eine Version auswählen.")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
self.is_loading = True
|
|
125
|
+
self.error_message = ""
|
|
126
|
+
try:
|
|
127
|
+
success = await SystemPromptRepository.delete(self.selected_version_id)
|
|
128
|
+
if success:
|
|
129
|
+
self.selected_version_id = 0
|
|
130
|
+
|
|
131
|
+
# Invalidate cache since latest version might have changed
|
|
132
|
+
await invalidate_prompt_cache()
|
|
133
|
+
logger.info("System prompt cache invalidated after deletion")
|
|
134
|
+
|
|
135
|
+
await self.load_versions()
|
|
136
|
+
yield rx.toast.success("Version erfolgreich gelöscht.")
|
|
137
|
+
else:
|
|
138
|
+
self.error_message = "Version nicht gefunden."
|
|
139
|
+
yield rx.toast.error("Version nicht gefunden.")
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
self.error_message = f"Fehler beim Löschen: {exc!s}"
|
|
142
|
+
logger.exception("Failed to delete version")
|
|
143
|
+
yield rx.toast.error(f"Fehler: {exc!s}")
|
|
144
|
+
finally:
|
|
145
|
+
self.is_loading = False
|
|
146
|
+
|
|
147
|
+
def set_current_prompt(self, value: str) -> None:
|
|
148
|
+
"""Update current prompt text and char count.
|
|
149
|
+
|
|
150
|
+
This is called on every keystroke but doesn't cause cursor jumping
|
|
151
|
+
because we use default_value in the textarea component.
|
|
152
|
+
"""
|
|
153
|
+
self.current_prompt = value
|
|
154
|
+
self.char_count = len(value)
|
|
155
|
+
|
|
156
|
+
def set_selected_version(self, value: str | None) -> None:
|
|
157
|
+
"""Handle version selection and load corresponding prompt.
|
|
158
|
+
|
|
159
|
+
When a version is selected, we update the prompt content and force
|
|
160
|
+
the textarea to re-render by changing its key.
|
|
161
|
+
"""
|
|
162
|
+
if value is None or value == "":
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
self.selected_version_id = int(value)
|
|
166
|
+
|
|
167
|
+
# Load the prompt for the selected version
|
|
168
|
+
if value in self.prompt_map:
|
|
169
|
+
self.current_prompt = self.prompt_map[value]
|
|
170
|
+
self.char_count = len(self.current_prompt)
|
|
171
|
+
# Force textarea to re-render with new content
|
|
172
|
+
self.textarea_key += 1
|
|
173
|
+
|
|
174
|
+
@rx.var
|
|
175
|
+
def selected_version_str(self) -> str:
|
|
176
|
+
"""Return selected version as string for the select component."""
|
|
177
|
+
if self.selected_version_id == 0:
|
|
178
|
+
return ""
|
|
179
|
+
return str(self.selected_version_id)
|