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.
@@ -0,0 +1,97 @@
1
+ """Component for MCP server selection modal."""
2
+
3
+ import reflex as rx
4
+
5
+ from appkit_assistant.backend.models import MCPServer
6
+ from appkit_assistant.state.thread_state import ThreadState
7
+
8
+
9
+ def render_mcp_server_item(server: MCPServer) -> rx.Component:
10
+ """Render a single MCP server item in the modal."""
11
+ return rx.hstack(
12
+ rx.switch(
13
+ checked=ThreadState.server_selection_state.get(server.id, False),
14
+ on_change=lambda checked: ThreadState.toggle_mcp_server_selection(
15
+ server.id, checked
16
+ ),
17
+ ),
18
+ rx.vstack(
19
+ rx.text(server.name, font_weight="bold", size="2"),
20
+ rx.text(server.description, size="1", color="gray"),
21
+ spacing="1",
22
+ align="start",
23
+ width="100%",
24
+ ),
25
+ width="100%",
26
+ )
27
+
28
+
29
+ def tools_popover() -> rx.Component:
30
+ """Render the tools modal popup."""
31
+ return rx.popover.root(
32
+ rx.popover.trigger(
33
+ rx.button(
34
+ rx.icon("pencil-ruler", size=18),
35
+ rx.text(
36
+ ThreadState.selected_mcp_servers.length().to_string()
37
+ + " von "
38
+ + ThreadState.available_mcp_servers.length().to_string(),
39
+ size="1",
40
+ ),
41
+ variant="ghost",
42
+ size="2",
43
+ border_radius="4px",
44
+ ),
45
+ ),
46
+ rx.popover.content(
47
+ rx.vstack(
48
+ rx.text("Werkzeuge verwalten", size="3", font_weight="bold"),
49
+ rx.cond(
50
+ ThreadState.available_mcp_servers.length() > 0,
51
+ rx.text(
52
+ "Wähle die Werkzeuge aus, die für diese Unterhaltung "
53
+ "verfügbar sein sollen.",
54
+ size="2",
55
+ color="gray",
56
+ margin_bottom="1.5em",
57
+ ),
58
+ rx.text(
59
+ "Es sind derzeit keine Werkzeuge verfügbar. "
60
+ "Bitte konfigurieren Sie MCP-Server in den Einstellungen.",
61
+ size="2",
62
+ color="gray",
63
+ margin_top="1.5em",
64
+ ),
65
+ ),
66
+ rx.scroll_area(
67
+ rx.vstack(
68
+ rx.foreach(
69
+ ThreadState.available_mcp_servers,
70
+ render_mcp_server_item,
71
+ ),
72
+ spacing="2",
73
+ width="100%",
74
+ ),
75
+ width="100%",
76
+ max_height="210px",
77
+ scrollbars="vertical",
78
+ type="auto",
79
+ ),
80
+ rx.button(
81
+ "Anwenden",
82
+ on_click=ThreadState.apply_mcp_server_selection,
83
+ variant="solid",
84
+ color_scheme="blue",
85
+ margin_top="1.5em",
86
+ ),
87
+ spacing="1",
88
+ ),
89
+ width="400px",
90
+ padding="1.5em",
91
+ align="end",
92
+ side="top",
93
+ ),
94
+ open=ThreadState.show_tools_modal,
95
+ on_open_change=ThreadState.set_show_tools_modal,
96
+ placement="bottom-start",
97
+ )
@@ -0,0 +1,10 @@
1
+ from pydantic import SecretStr
2
+
3
+ from appkit_commons.configuration import BaseConfig
4
+
5
+
6
+ class AssistantConfig(BaseConfig):
7
+ perplexity_api_key: SecretStr | None = None
8
+ openai_base_url: str | None = None
9
+ openai_api_key: SecretStr | None = None
10
+ google_api_key: SecretStr | None = None
@@ -0,0 +1,222 @@
1
+ """State management for MCP servers."""
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import AsyncGenerator
6
+ from typing import Any
7
+
8
+ import reflex as rx
9
+
10
+ from appkit_assistant.backend.models import MCPServer
11
+ from appkit_assistant.backend.repositories import (
12
+ MCPServerRepository,
13
+ )
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MCPServerState(rx.State):
19
+ """State class for managing MCP servers."""
20
+
21
+ servers: list[MCPServer] = []
22
+ current_server: MCPServer | None = None
23
+ loading: bool = False
24
+
25
+ async def load_servers(self) -> None:
26
+ """Load all MCP servers from the database.
27
+
28
+ Raises exceptions to let callers decide how to handle errors.
29
+ """
30
+ self.loading = True
31
+ try:
32
+ self.servers = await MCPServerRepository.get_all()
33
+ logger.debug("Loaded %d MCP servers", len(self.servers))
34
+ except Exception as e:
35
+ logger.error("Failed to load MCP servers: %s", e)
36
+ raise
37
+ finally:
38
+ self.loading = False
39
+
40
+ async def load_servers_with_toast(self) -> AsyncGenerator[Any, Any]:
41
+ """Load servers and show an error toast on failure."""
42
+ try:
43
+ await self.load_servers()
44
+ except Exception:
45
+ yield rx.toast.error(
46
+ "Fehler beim Laden der MCP Server.",
47
+ position="top-right",
48
+ )
49
+
50
+ async def get_server(self, server_id: int) -> None:
51
+ """Get a specific MCP server by ID."""
52
+ try:
53
+ self.current_server = await MCPServerRepository.get_by_id(server_id)
54
+ if not self.current_server:
55
+ logger.warning("MCP server with ID %d not found", server_id)
56
+ except Exception as e:
57
+ logger.error("Failed to get MCP server %d: %s", server_id, e)
58
+
59
+ async def set_current_server(self, server: MCPServer) -> None:
60
+ """Set the current server."""
61
+ self.current_server = server
62
+
63
+ async def add_server(self, form_data: dict[str, Any]) -> AsyncGenerator[Any, Any]:
64
+ """Add a new MCP server."""
65
+ try:
66
+ headers = self._parse_headers_from_form(form_data)
67
+ server = await MCPServerRepository.create(
68
+ name=form_data["name"],
69
+ url=form_data["url"],
70
+ headers=headers,
71
+ description=form_data.get("description") or None,
72
+ prompt=form_data.get("prompt") or None,
73
+ )
74
+
75
+ await self.load_servers()
76
+ yield rx.toast.info(
77
+ "MCP Server {} wurde hinzugefügt.".format(form_data["name"]),
78
+ position="top-right",
79
+ )
80
+ logger.debug("Added MCP server: %s", server.name)
81
+
82
+ except ValueError as e:
83
+ logger.error("Invalid form data for MCP server: %s", e)
84
+ yield rx.toast.error(
85
+ str(e),
86
+ position="top-right",
87
+ )
88
+ except Exception as e:
89
+ logger.error("Failed to add MCP server: %s", e)
90
+ yield rx.toast.error(
91
+ "Fehler beim Hinzufügen des MCP Servers.",
92
+ position="top-right",
93
+ )
94
+
95
+ async def modify_server(
96
+ self, form_data: dict[str, Any]
97
+ ) -> AsyncGenerator[Any, Any]:
98
+ """Modify an existing MCP server."""
99
+ if not self.current_server:
100
+ yield rx.toast.error(
101
+ "Kein Server ausgewählt.",
102
+ position="top-right",
103
+ )
104
+ return
105
+
106
+ try:
107
+ headers = self._parse_headers_from_form(form_data)
108
+ updated_server = await MCPServerRepository.update(
109
+ server_id=self.current_server.id,
110
+ name=form_data["name"],
111
+ url=form_data["url"],
112
+ headers=headers,
113
+ description=form_data.get("description") or None,
114
+ prompt=form_data.get("prompt") or None,
115
+ )
116
+
117
+ if updated_server:
118
+ await self.load_servers()
119
+ yield rx.toast.info(
120
+ "MCP Server {} wurde aktualisiert.".format(form_data["name"]),
121
+ position="top-right",
122
+ )
123
+ logger.debug("Updated MCP server: %s", updated_server.name)
124
+ else:
125
+ yield rx.toast.error(
126
+ "MCP Server konnte nicht gefunden werden.",
127
+ position="top-right",
128
+ )
129
+
130
+ except ValueError as e:
131
+ logger.error("Invalid form data for MCP server: %s", e)
132
+ yield rx.toast.error(
133
+ str(e),
134
+ position="top-right",
135
+ )
136
+ except Exception as e:
137
+ logger.error("Failed to update MCP server: %s", e)
138
+ yield rx.toast.error(
139
+ "Fehler beim Aktualisieren des MCP Servers.",
140
+ position="top-right",
141
+ )
142
+
143
+ async def delete_server(self, server_id: int) -> AsyncGenerator[Any, Any]:
144
+ """Delete an MCP server."""
145
+ try:
146
+ # Get server name for the success message
147
+ server = await MCPServerRepository.get_by_id(server_id)
148
+ if not server:
149
+ yield rx.toast.error(
150
+ "MCP Server nicht gefunden.",
151
+ position="top-right",
152
+ )
153
+ return
154
+
155
+ server_name = server.name
156
+
157
+ # Delete server using repository
158
+ success = await MCPServerRepository.delete(server_id)
159
+
160
+ if success:
161
+ await self.load_servers()
162
+ yield rx.toast.info(
163
+ f"MCP Server {server_name} wurde gelöscht.",
164
+ position="top-right",
165
+ )
166
+ logger.debug("Deleted MCP server: %s", server_name)
167
+ else:
168
+ yield rx.toast.error(
169
+ "MCP Server konnte nicht gelöscht werden.",
170
+ position="top-right",
171
+ )
172
+
173
+ except Exception as e:
174
+ logger.error("Failed to delete MCP server %d: %s", server_id, e)
175
+ yield rx.toast.error(
176
+ "Fehler beim Löschen des MCP Servers.",
177
+ position="top-right",
178
+ )
179
+
180
+ def _parse_headers_from_form(self, form_data: dict[str, Any]) -> dict[str, str]:
181
+ """Parse headers from form data."""
182
+ headers_json = form_data.get("headers_json", "").strip()
183
+ if not headers_json:
184
+ return "{}"
185
+
186
+ try:
187
+ headers = json.loads(headers_json)
188
+ if not isinstance(headers, dict):
189
+ logger.warning("Headers JSON is not a dictionary: %s", headers_json)
190
+ raise ValueError("Headers JSON must be a dictionary")
191
+
192
+ # Ensure all keys and values are strings
193
+ cleaned_headers = {}
194
+ for key, value in headers.items():
195
+ if isinstance(key, str) and isinstance(value, str):
196
+ cleaned_headers[key] = value
197
+ else:
198
+ logger.warning("Invalid header key-value pair: %s=%s", key, value)
199
+ raise ValueError(f"Invalid header key-value pair: {key}={value}")
200
+
201
+ logger.debug("Parsed headers from JSON: %s", cleaned_headers)
202
+ return headers_json
203
+
204
+ except json.JSONDecodeError as e:
205
+ logger.error("Invalid JSON in headers field: %s", e)
206
+ raise ValueError(
207
+ "Ungültiges JSON-Format in den HTTP-Headern. "
208
+ "Bitte überprüfen Sie die Eingabe."
209
+ ) from e
210
+ except ValueError:
211
+ # Re-raise ValueError exceptions (invalid dictionary or key-value pairs)
212
+ raise
213
+
214
+ @rx.var
215
+ def server_count(self) -> int:
216
+ """Get the number of servers."""
217
+ return len(self.servers)
218
+
219
+ @rx.var
220
+ def has_servers(self) -> bool:
221
+ """Check if there are any servers."""
222
+ return len(self.servers) > 0