appkit-assistant 0.7.1__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/model_manager.py +133 -0
- appkit_assistant/backend/models.py +103 -0
- appkit_assistant/backend/processor.py +46 -0
- appkit_assistant/backend/processors/ai_models.py +109 -0
- appkit_assistant/backend/processors/knowledgeai_processor.py +275 -0
- appkit_assistant/backend/processors/lorem_ipsum_processor.py +123 -0
- appkit_assistant/backend/processors/openai_base.py +73 -0
- appkit_assistant/backend/processors/openai_chat_completion_processor.py +117 -0
- appkit_assistant/backend/processors/openai_responses_processor.py +508 -0
- appkit_assistant/backend/processors/perplexity_processor.py +118 -0
- appkit_assistant/backend/repositories.py +96 -0
- appkit_assistant/backend/system_prompt.py +56 -0
- appkit_assistant/components/__init__.py +38 -0
- appkit_assistant/components/composer.py +154 -0
- appkit_assistant/components/composer_key_handler.py +38 -0
- appkit_assistant/components/mcp_server_dialogs.py +344 -0
- appkit_assistant/components/mcp_server_table.py +76 -0
- appkit_assistant/components/message.py +299 -0
- appkit_assistant/components/thread.py +252 -0
- appkit_assistant/components/threadlist.py +134 -0
- appkit_assistant/components/tools_modal.py +97 -0
- appkit_assistant/configuration.py +10 -0
- appkit_assistant/state/mcp_server_state.py +222 -0
- appkit_assistant/state/thread_state.py +874 -0
- appkit_assistant-0.7.1.dist-info/METADATA +8 -0
- appkit_assistant-0.7.1.dist-info/RECORD +27 -0
- appkit_assistant-0.7.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""Dialog components for MCP server management."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import reflex as rx
|
|
7
|
+
from reflex.vars import var_operation, var_operation_return
|
|
8
|
+
from reflex.vars.base import RETURN, CustomVarOperationReturn
|
|
9
|
+
|
|
10
|
+
import appkit_mantine as mn
|
|
11
|
+
from appkit_assistant.backend.models import MCPServer
|
|
12
|
+
from appkit_assistant.state.mcp_server_state import MCPServerState
|
|
13
|
+
from appkit_ui.components.dialogs import (
|
|
14
|
+
delete_dialog,
|
|
15
|
+
dialog_buttons,
|
|
16
|
+
dialog_header,
|
|
17
|
+
)
|
|
18
|
+
from appkit_ui.components.form_inputs import form_field
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationState(rx.State):
|
|
24
|
+
url: str = ""
|
|
25
|
+
name: str = ""
|
|
26
|
+
desciption: str = ""
|
|
27
|
+
prompt: str = ""
|
|
28
|
+
|
|
29
|
+
url_error: str = ""
|
|
30
|
+
name_error: str = ""
|
|
31
|
+
description_error: str = ""
|
|
32
|
+
prompt_error: str = ""
|
|
33
|
+
|
|
34
|
+
@rx.event
|
|
35
|
+
def initialize(self, server: MCPServer | None = None) -> None:
|
|
36
|
+
"""Reset validation state."""
|
|
37
|
+
logger.debug("Initializing ValidationState")
|
|
38
|
+
if server is None:
|
|
39
|
+
self.url = ""
|
|
40
|
+
self.name = ""
|
|
41
|
+
self.desciption = ""
|
|
42
|
+
self.prompt = ""
|
|
43
|
+
else:
|
|
44
|
+
self.url = server.url
|
|
45
|
+
self.name = server.name
|
|
46
|
+
self.desciption = server.description
|
|
47
|
+
self.prompt = server.prompt or ""
|
|
48
|
+
|
|
49
|
+
self.url_error = ""
|
|
50
|
+
self.name_error = ""
|
|
51
|
+
self.description_error = ""
|
|
52
|
+
self.prompt_error = ""
|
|
53
|
+
|
|
54
|
+
@rx.event
|
|
55
|
+
def validate_url(self) -> None:
|
|
56
|
+
"""Validate the URL field."""
|
|
57
|
+
if not self.url or self.url.strip() == "":
|
|
58
|
+
self.url_error = "Die URL darf nicht leer sein."
|
|
59
|
+
elif not self.url.startswith("http://") and not self.url.startswith("https://"):
|
|
60
|
+
self.url_error = "Die URL muss mit http:// oder https:// beginnen."
|
|
61
|
+
else:
|
|
62
|
+
self.url_error = ""
|
|
63
|
+
|
|
64
|
+
@rx.event
|
|
65
|
+
def validate_name(self) -> None:
|
|
66
|
+
"""Validate the name field."""
|
|
67
|
+
if not self.name or self.name.strip() == "":
|
|
68
|
+
self.name_error = "Der Name darf nicht leer sein."
|
|
69
|
+
elif len(self.name) < 3: # noqa: PLR2004
|
|
70
|
+
self.name_error = "Der Name muss mindestens 3 Zeichen lang sein."
|
|
71
|
+
else:
|
|
72
|
+
self.name_error = ""
|
|
73
|
+
|
|
74
|
+
@rx.event
|
|
75
|
+
def validate_description(self) -> None:
|
|
76
|
+
"""Validate the description field."""
|
|
77
|
+
if self.desciption and len(self.desciption) > 200: # noqa: PLR2004
|
|
78
|
+
self.description_error = (
|
|
79
|
+
"Die Beschreibung darf maximal 200 Zeichen lang sein."
|
|
80
|
+
)
|
|
81
|
+
elif not self.desciption or self.desciption.strip() == "":
|
|
82
|
+
self.description_error = "Die Beschreibung darf nicht leer sein."
|
|
83
|
+
else:
|
|
84
|
+
self.description_error = ""
|
|
85
|
+
|
|
86
|
+
@rx.event
|
|
87
|
+
def validate_prompt(self) -> None:
|
|
88
|
+
"""Validate the prompt field."""
|
|
89
|
+
if self.prompt and len(self.prompt) > 2000: # noqa: PLR2004
|
|
90
|
+
self.prompt_error = "Die Anweisung darf maximal 2000 Zeichen lang sein."
|
|
91
|
+
else:
|
|
92
|
+
self.prompt_error = ""
|
|
93
|
+
|
|
94
|
+
@rx.var
|
|
95
|
+
def has_errors(self) -> bool:
|
|
96
|
+
"""Check if the form can be submitted."""
|
|
97
|
+
errors = bool(
|
|
98
|
+
self.url_error
|
|
99
|
+
or self.name_error
|
|
100
|
+
or self.description_error
|
|
101
|
+
or self.prompt_error
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
logger.debug("Has validation errors: %s", errors)
|
|
105
|
+
return errors
|
|
106
|
+
|
|
107
|
+
@rx.var
|
|
108
|
+
def prompt_remaining(self) -> int:
|
|
109
|
+
"""Calculate remaining characters for prompt field."""
|
|
110
|
+
return 2000 - len(self.prompt or "")
|
|
111
|
+
|
|
112
|
+
def set_url(self, url: str) -> None:
|
|
113
|
+
"""Set the URL and validate it."""
|
|
114
|
+
self.url = url
|
|
115
|
+
self.validate_url()
|
|
116
|
+
|
|
117
|
+
def set_name(self, name: str) -> None:
|
|
118
|
+
"""Set the name and validate it."""
|
|
119
|
+
self.name = name
|
|
120
|
+
self.validate_name()
|
|
121
|
+
|
|
122
|
+
def set_description(self, description: str) -> None:
|
|
123
|
+
"""Set the description and validate it."""
|
|
124
|
+
self.desciption = description
|
|
125
|
+
self.validate_description()
|
|
126
|
+
|
|
127
|
+
def set_prompt(self, prompt: str) -> None:
|
|
128
|
+
"""Set the prompt and validate it."""
|
|
129
|
+
self.prompt = prompt
|
|
130
|
+
self.validate_prompt()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@var_operation
|
|
134
|
+
def json(obj: rx.Var, indent: int = 4) -> CustomVarOperationReturn[RETURN]:
|
|
135
|
+
return var_operation_return(
|
|
136
|
+
js_expression=f"JSON.stringify(JSON.parse({obj}), null, {indent})",
|
|
137
|
+
var_type=Any,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def mcp_server_form_fields(server: MCPServer | None = None) -> rx.Component:
|
|
142
|
+
"""Reusable form fields for MCP server add/update dialogs."""
|
|
143
|
+
is_edit_mode = server is not None
|
|
144
|
+
|
|
145
|
+
fields = [
|
|
146
|
+
form_field(
|
|
147
|
+
name="name",
|
|
148
|
+
icon="server",
|
|
149
|
+
label="Name",
|
|
150
|
+
hint="Eindeutiger Name des MCP-Servers",
|
|
151
|
+
type="text",
|
|
152
|
+
placeholder="MCP-Server Name",
|
|
153
|
+
default_value=server.name if is_edit_mode else "",
|
|
154
|
+
required=True,
|
|
155
|
+
max_length=64,
|
|
156
|
+
on_change=ValidationState.set_name,
|
|
157
|
+
on_blur=ValidationState.validate_name,
|
|
158
|
+
validation_error=ValidationState.name_error,
|
|
159
|
+
),
|
|
160
|
+
form_field(
|
|
161
|
+
name="description",
|
|
162
|
+
icon="text",
|
|
163
|
+
label="Beschreibung",
|
|
164
|
+
hint=(
|
|
165
|
+
"Kurze Beschreibung zur besseren Identifikation und Auswahl "
|
|
166
|
+
"durch den Nutzer"
|
|
167
|
+
),
|
|
168
|
+
type="text",
|
|
169
|
+
placeholder="Beschreibung...",
|
|
170
|
+
max_length=200,
|
|
171
|
+
default_value=server.description if is_edit_mode else "",
|
|
172
|
+
required=True,
|
|
173
|
+
on_change=ValidationState.set_description,
|
|
174
|
+
on_blur=ValidationState.validate_description,
|
|
175
|
+
validation_error=ValidationState.description_error,
|
|
176
|
+
),
|
|
177
|
+
form_field(
|
|
178
|
+
name="url",
|
|
179
|
+
icon="link",
|
|
180
|
+
label="URL",
|
|
181
|
+
hint="Vollständige URL des MCP-Servers (z. B. https://example.com/mcp/v1/sse)",
|
|
182
|
+
type="text",
|
|
183
|
+
placeholder="https://example.com/mcp/v1/sse",
|
|
184
|
+
default_value=server.url if is_edit_mode else "",
|
|
185
|
+
required=True,
|
|
186
|
+
on_change=ValidationState.set_url,
|
|
187
|
+
on_blur=ValidationState.validate_url,
|
|
188
|
+
validation_error=ValidationState.url_error,
|
|
189
|
+
),
|
|
190
|
+
rx.flex(
|
|
191
|
+
mn.textarea(
|
|
192
|
+
name="prompt",
|
|
193
|
+
label="Prompt",
|
|
194
|
+
description=(
|
|
195
|
+
"Beschreiben Sie, wie das MCP-Tool verwendet werden soll. "
|
|
196
|
+
"Dies wird als Ergänzung des Systemprompts im Chat genutzt."
|
|
197
|
+
),
|
|
198
|
+
placeholder=("Anweidungen an das Modell..."),
|
|
199
|
+
default_value=server.prompt if is_edit_mode else "",
|
|
200
|
+
on_change=ValidationState.set_prompt,
|
|
201
|
+
on_blur=ValidationState.validate_prompt,
|
|
202
|
+
validation_error=ValidationState.prompt_error,
|
|
203
|
+
autosize=True,
|
|
204
|
+
min_rows=3,
|
|
205
|
+
max_rows=8,
|
|
206
|
+
width="100%",
|
|
207
|
+
),
|
|
208
|
+
rx.flex(
|
|
209
|
+
rx.cond(
|
|
210
|
+
ValidationState.prompt_remaining >= 0,
|
|
211
|
+
rx.text(
|
|
212
|
+
f"{ValidationState.prompt_remaining}/2000",
|
|
213
|
+
size="1",
|
|
214
|
+
color="gray",
|
|
215
|
+
),
|
|
216
|
+
rx.text(
|
|
217
|
+
f"{ValidationState.prompt_remaining}/2000",
|
|
218
|
+
size="1",
|
|
219
|
+
color="red",
|
|
220
|
+
weight="bold",
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
justify="end",
|
|
224
|
+
width="100%",
|
|
225
|
+
margin_top="4px",
|
|
226
|
+
),
|
|
227
|
+
direction="column",
|
|
228
|
+
spacing="0",
|
|
229
|
+
width="100%",
|
|
230
|
+
),
|
|
231
|
+
mn.form.json(
|
|
232
|
+
name="headers_json",
|
|
233
|
+
label="HTTP Headers",
|
|
234
|
+
description=(
|
|
235
|
+
"Geben Sie die HTTP-Header im JSON-Format ein. "
|
|
236
|
+
'Beispiel: {"Content-Type": "application/json", '
|
|
237
|
+
'"Authorization": "Bearer token"}'
|
|
238
|
+
),
|
|
239
|
+
placeholder="{}",
|
|
240
|
+
validation_error="Ungültiges JSON",
|
|
241
|
+
default_value=json(server.headers) if is_edit_mode else "{}",
|
|
242
|
+
format_on_blur=True,
|
|
243
|
+
autosize=True,
|
|
244
|
+
min_rows=4,
|
|
245
|
+
max_rows=6,
|
|
246
|
+
width="100%",
|
|
247
|
+
),
|
|
248
|
+
]
|
|
249
|
+
|
|
250
|
+
return rx.flex(
|
|
251
|
+
*fields,
|
|
252
|
+
direction="column",
|
|
253
|
+
spacing="1",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def add_mcp_server_button() -> rx.Component:
|
|
258
|
+
"""Button and dialog for adding a new MCP server."""
|
|
259
|
+
ValidationState.initialize()
|
|
260
|
+
return rx.dialog.root(
|
|
261
|
+
rx.dialog.trigger(
|
|
262
|
+
rx.button(
|
|
263
|
+
rx.icon("plus"),
|
|
264
|
+
rx.text("Neuen MCP Server anlegen", display=["none", "none", "block"]),
|
|
265
|
+
size="3",
|
|
266
|
+
variant="solid",
|
|
267
|
+
on_click=[ValidationState.initialize(server=None)],
|
|
268
|
+
),
|
|
269
|
+
),
|
|
270
|
+
rx.dialog.content(
|
|
271
|
+
dialog_header(
|
|
272
|
+
icon="server",
|
|
273
|
+
title="Neuen MCP Server anlegen",
|
|
274
|
+
description="Geben Sie die Details des neuen MCP Servers ein",
|
|
275
|
+
),
|
|
276
|
+
rx.flex(
|
|
277
|
+
rx.form.root(
|
|
278
|
+
mcp_server_form_fields(),
|
|
279
|
+
dialog_buttons(
|
|
280
|
+
"MCP Server anlegen",
|
|
281
|
+
has_errors=ValidationState.has_errors,
|
|
282
|
+
),
|
|
283
|
+
on_submit=MCPServerState.add_server,
|
|
284
|
+
reset_on_submit=False,
|
|
285
|
+
),
|
|
286
|
+
width="100%",
|
|
287
|
+
direction="column",
|
|
288
|
+
spacing="4",
|
|
289
|
+
),
|
|
290
|
+
class_name="dialog",
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def delete_mcp_server_dialog(server: MCPServer) -> rx.Component:
|
|
296
|
+
"""Use the generic delete dialog component for MCP servers."""
|
|
297
|
+
return delete_dialog(
|
|
298
|
+
title="MCP Server löschen",
|
|
299
|
+
content=server.name,
|
|
300
|
+
on_click=lambda: MCPServerState.delete_server(server.id),
|
|
301
|
+
icon_button=True,
|
|
302
|
+
size="2",
|
|
303
|
+
variant="ghost",
|
|
304
|
+
color_scheme="crimson",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def update_mcp_server_dialog(server: MCPServer) -> rx.Component:
|
|
309
|
+
"""Dialog for updating an existing MCP server."""
|
|
310
|
+
return rx.dialog.root(
|
|
311
|
+
rx.dialog.trigger(
|
|
312
|
+
rx.icon_button(
|
|
313
|
+
rx.icon("square-pen", size=20),
|
|
314
|
+
size="2",
|
|
315
|
+
variant="ghost",
|
|
316
|
+
on_click=[
|
|
317
|
+
lambda: MCPServerState.get_server(server.id),
|
|
318
|
+
ValidationState.initialize(server),
|
|
319
|
+
],
|
|
320
|
+
),
|
|
321
|
+
),
|
|
322
|
+
rx.dialog.content(
|
|
323
|
+
dialog_header(
|
|
324
|
+
icon="server",
|
|
325
|
+
title="MCP Server aktualisieren",
|
|
326
|
+
description="Aktualisieren Sie die Details des MCP Servers",
|
|
327
|
+
),
|
|
328
|
+
rx.flex(
|
|
329
|
+
rx.form.root(
|
|
330
|
+
mcp_server_form_fields(server),
|
|
331
|
+
dialog_buttons(
|
|
332
|
+
"MCP Server aktualisieren",
|
|
333
|
+
has_errors=ValidationState.has_errors,
|
|
334
|
+
),
|
|
335
|
+
on_submit=MCPServerState.modify_server,
|
|
336
|
+
reset_on_submit=False,
|
|
337
|
+
),
|
|
338
|
+
width="100%",
|
|
339
|
+
direction="column",
|
|
340
|
+
spacing="4",
|
|
341
|
+
),
|
|
342
|
+
class_name="dialog",
|
|
343
|
+
),
|
|
344
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Table component for displaying MCP servers."""
|
|
2
|
+
|
|
3
|
+
import reflex as rx
|
|
4
|
+
from reflex.components.radix.themes.components.table import TableRow
|
|
5
|
+
|
|
6
|
+
from appkit_assistant.backend.models import MCPServer
|
|
7
|
+
from appkit_assistant.components.mcp_server_dialogs import (
|
|
8
|
+
add_mcp_server_button,
|
|
9
|
+
delete_mcp_server_dialog,
|
|
10
|
+
update_mcp_server_dialog,
|
|
11
|
+
)
|
|
12
|
+
from appkit_assistant.state.mcp_server_state import MCPServerState
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def mcp_server_table_row(server: MCPServer) -> TableRow:
|
|
16
|
+
"""Show an MCP server in a table row."""
|
|
17
|
+
return rx.table.row(
|
|
18
|
+
rx.table.cell(
|
|
19
|
+
server.name,
|
|
20
|
+
white_space="nowrap",
|
|
21
|
+
),
|
|
22
|
+
rx.table.cell(
|
|
23
|
+
rx.text(
|
|
24
|
+
server.description,
|
|
25
|
+
title=server.description,
|
|
26
|
+
style={
|
|
27
|
+
"display": "block",
|
|
28
|
+
"overflow": "hidden",
|
|
29
|
+
"text_overflow": "ellipsis",
|
|
30
|
+
"white_space": "nowrap",
|
|
31
|
+
},
|
|
32
|
+
),
|
|
33
|
+
white_space="nowrap",
|
|
34
|
+
style={
|
|
35
|
+
"max_width": "0",
|
|
36
|
+
"width": "100%",
|
|
37
|
+
},
|
|
38
|
+
),
|
|
39
|
+
rx.table.cell(
|
|
40
|
+
rx.hstack(
|
|
41
|
+
update_mcp_server_dialog(server),
|
|
42
|
+
delete_mcp_server_dialog(server),
|
|
43
|
+
spacing="2",
|
|
44
|
+
align_items="center",
|
|
45
|
+
),
|
|
46
|
+
white_space="nowrap",
|
|
47
|
+
),
|
|
48
|
+
justify="center",
|
|
49
|
+
vertical_align="middle",
|
|
50
|
+
style={"_hover": {"bg": rx.color("gray", 2)}},
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def mcp_servers_table() -> rx.Fragment:
|
|
55
|
+
return rx.fragment(
|
|
56
|
+
rx.flex(
|
|
57
|
+
add_mcp_server_button(),
|
|
58
|
+
rx.spacer(),
|
|
59
|
+
),
|
|
60
|
+
rx.table.root(
|
|
61
|
+
rx.table.header(
|
|
62
|
+
rx.table.row(
|
|
63
|
+
rx.table.column_header_cell("Name", width="20%"),
|
|
64
|
+
rx.table.column_header_cell(
|
|
65
|
+
"Beschreibung", width="calc(80% - 140px)"
|
|
66
|
+
),
|
|
67
|
+
rx.table.column_header_cell("", width="140px"),
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
rx.table.body(rx.foreach(MCPServerState.servers, mcp_server_table_row)),
|
|
71
|
+
size="3",
|
|
72
|
+
width="100%",
|
|
73
|
+
table_layout="fixed",
|
|
74
|
+
on_mount=MCPServerState.load_servers_with_toast,
|
|
75
|
+
),
|
|
76
|
+
)
|