appkit-assistant 0.15.4__tar.gz → 0.16.1__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 (36) hide show
  1. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/PKG-INFO +1 -1
  2. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/pyproject.toml +1 -1
  3. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/processor.py +19 -0
  4. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/processors/openai_responses_processor.py +6 -2
  5. appkit_assistant-0.16.1/src/appkit_assistant/components/mcp_oauth.py +46 -0
  6. appkit_assistant-0.16.1/src/appkit_assistant/pages.py +17 -0
  7. appkit_assistant-0.16.1/src/appkit_assistant/state/mcp_oauth_state.py +222 -0
  8. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/.gitignore +0 -0
  9. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/README.md +0 -0
  10. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/docs/assistant.png +0 -0
  11. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/mcp_auth_service.py +0 -0
  12. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/model_manager.py +0 -0
  13. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/models.py +0 -0
  14. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +0 -0
  15. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/processors/openai_base.py +0 -0
  16. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +0 -0
  17. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
  18. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/repositories.py +0 -0
  19. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/services/thread_service.py +0 -0
  20. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/backend/system_prompt_cache.py +0 -0
  21. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/__init__.py +0 -0
  22. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/composer.py +0 -0
  23. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/composer_key_handler.py +0 -0
  24. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/mcp_server_dialogs.py +0 -0
  25. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/mcp_server_table.py +0 -0
  26. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/message.py +0 -0
  27. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/system_prompt_editor.py +0 -0
  28. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/thread.py +0 -0
  29. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/threadlist.py +0 -0
  30. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/components/tools_modal.py +0 -0
  31. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/configuration.py +0 -0
  32. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/logic/response_accumulator.py +0 -0
  33. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/state/mcp_server_state.py +0 -0
  34. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/state/system_prompt_state.py +0 -0
  35. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/state/thread_list_state.py +0 -0
  36. {appkit_assistant-0.15.4 → appkit_assistant-0.16.1}/src/appkit_assistant/state/thread_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appkit-assistant
3
- Version: 0.15.4
3
+ Version: 0.16.1
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.4"
10
+ version = "0.16.1"
11
11
  description = "Add your description here"
12
12
  readme = "README.md"
13
13
  authors = [{ name = "Jens Rehpöhler" }]
@@ -7,9 +7,28 @@ import logging
7
7
  from collections.abc import AsyncGenerator
8
8
 
9
9
  from appkit_assistant.backend.models import AIModel, Chunk, MCPServer, Message
10
+ from appkit_commons.configuration.configuration import ReflexConfig
11
+ from appkit_commons.registry import service_registry
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
15
+ # OAuth callback path - must match registered redirect URIs
16
+ MCP_OAUTH_CALLBACK_PATH = "/assistant/mcp/callback"
17
+
18
+
19
+ def mcp_oauth_redirect_uri() -> str:
20
+ """Build the MCP OAuth redirect URI from configuration."""
21
+ reflex_config: ReflexConfig | None = service_registry().get(ReflexConfig)
22
+ if reflex_config:
23
+ base_url = reflex_config.deploy_url
24
+ port = reflex_config.frontend_port
25
+ # Only add port if not standard (80 for http, 443 for https)
26
+ if port and port not in (80, 443):
27
+ return f"{base_url}:{port}{MCP_OAUTH_CALLBACK_PATH}"
28
+ return f"{base_url}{MCP_OAUTH_CALLBACK_PATH}"
29
+ # Fallback for development
30
+ return f"http://localhost:8080{MCP_OAUTH_CALLBACK_PATH}"
31
+
13
32
 
14
33
  class Processor(abc.ABC):
15
34
  """Base processor interface for AI processing services."""
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import logging
3
3
  from collections.abc import AsyncGenerator
4
- from typing import Any
4
+ from typing import Any, Final
5
5
 
6
6
  import reflex as rx
7
7
 
@@ -16,11 +16,13 @@ from appkit_assistant.backend.models import (
16
16
  Message,
17
17
  MessageType,
18
18
  )
19
+ from appkit_assistant.backend.processor import mcp_oauth_redirect_uri
19
20
  from appkit_assistant.backend.processors.openai_base import BaseOpenAIProcessor
20
21
  from appkit_assistant.backend.system_prompt_cache import get_system_prompt
21
22
  from appkit_commons.database.session import get_session_manager
22
23
 
23
24
  logger = logging.getLogger(__name__)
25
+ default_oauth_redirect_uri: Final[str] = mcp_oauth_redirect_uri()
24
26
 
25
27
 
26
28
  class OpenAIResponsesProcessor(BaseOpenAIProcessor):
@@ -32,7 +34,7 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
32
34
  api_key: str | None = None,
33
35
  base_url: str | None = None,
34
36
  is_azure: bool = False,
35
- oauth_redirect_uri: str = "",
37
+ oauth_redirect_uri: str = default_oauth_redirect_uri,
36
38
  ) -> None:
37
39
  super().__init__(models, api_key, base_url, is_azure)
38
40
  self._current_reasoning_session: str | None = None
@@ -40,6 +42,8 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
40
42
  self._mcp_auth_service = MCPAuthService(redirect_uri=oauth_redirect_uri)
41
43
  self._pending_auth_servers: list[MCPServer] = []
42
44
 
45
+ logger.debug("Using redirect URI for MCP OAuth: %s", oauth_redirect_uri)
46
+
43
47
  async def process(
44
48
  self,
45
49
  messages: list[Message],
@@ -0,0 +1,46 @@
1
+ import reflex as rx
2
+
3
+ from appkit_assistant.state.mcp_oauth_state import MCPOAuthState
4
+
5
+
6
+ def mcp_oauth_callback_content() -> rx.Component:
7
+ """Content for the MCP OAuth callback page."""
8
+ return rx.center(
9
+ rx.card(
10
+ rx.vstack(
11
+ rx.cond(
12
+ MCPOAuthState.status == "processing",
13
+ rx.fragment(
14
+ rx.spinner(size="3"),
15
+ rx.text(MCPOAuthState.message, size="3"),
16
+ ),
17
+ rx.cond(
18
+ MCPOAuthState.status == "success",
19
+ rx.fragment(
20
+ rx.icon("circle-check", size=48, color="green"),
21
+ rx.text(MCPOAuthState.message, size="3", weight="medium"),
22
+ rx.text(
23
+ "Dieses Fenster wird automatisch geschlossen.",
24
+ size="2",
25
+ color="gray",
26
+ ),
27
+ ),
28
+ rx.fragment(
29
+ rx.icon("circle-alert", size=48, color="red"),
30
+ rx.text(MCPOAuthState.message, size="3", weight="medium"),
31
+ rx.button(
32
+ "Fenster schließen",
33
+ on_click=rx.call_script("window.close()"),
34
+ variant="soft",
35
+ ),
36
+ ),
37
+ ),
38
+ ),
39
+ align="center",
40
+ spacing="4",
41
+ padding="6",
42
+ ),
43
+ size="3",
44
+ ),
45
+ height="100vh",
46
+ )
@@ -0,0 +1,17 @@
1
+ import reflex as rx
2
+
3
+ from appkit_assistant.components.mcp_oauth import mcp_oauth_callback_content
4
+ from appkit_assistant.state.mcp_oauth_state import MCPOAuthState
5
+
6
+
7
+ @rx.page(
8
+ route="/assistant/mcp/callback",
9
+ title="MCP Verbindung",
10
+ on_load=MCPOAuthState.handle_mcp_oauth_callback,
11
+ )
12
+ def mcp_oauth_callback_page() -> rx.Component:
13
+ """MCP OAuth callback page."""
14
+ return rx.theme(
15
+ mcp_oauth_callback_content(),
16
+ has_background=True,
17
+ )
@@ -0,0 +1,222 @@
1
+ """MCP OAuth callback page and state.
2
+
3
+ Handles OAuth redirects from MCP server identity providers.
4
+ """
5
+
6
+ import contextlib
7
+ import logging
8
+ from collections.abc import AsyncGenerator
9
+
10
+ import reflex as rx
11
+ from sqlmodel import Session, select
12
+
13
+ from appkit_assistant.backend.mcp_auth_service import MCPAuthService
14
+ from appkit_assistant.backend.models import MCPServer
15
+ from appkit_assistant.backend.processor import mcp_oauth_redirect_uri
16
+ from appkit_commons.database.session import get_session_manager
17
+ from appkit_user.authentication.backend.entities import OAuthStateEntity
18
+ from appkit_user.authentication.states import UserSession
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class MCPOAuthState(rx.State):
24
+ """State for handling MCP OAuth callbacks."""
25
+
26
+ # UI state
27
+ status: str = "processing" # processing, success, error
28
+ message: str = "Verarbeite Anmeldung..."
29
+ server_name: str = ""
30
+
31
+ # Stored from URL params
32
+ _code: str = ""
33
+ _state: str = ""
34
+ _server_id: int | None = None
35
+
36
+ @rx.event
37
+ async def handle_mcp_oauth_callback(self) -> AsyncGenerator:
38
+ """Handle the OAuth callback from an MCP server's identity provider."""
39
+ # Get query params from router (using new router.url API)
40
+ params = self.router.url.query_parameters
41
+ code = params.get("code", "")
42
+ state = params.get("state", "")
43
+ server_id_str = params.get("server_id", "")
44
+
45
+ logger.info(
46
+ "MCP OAuth callback - params: %s, code: '%s', state: '%s', server_id: '%s'",
47
+ params,
48
+ code[:20] if code else "None",
49
+ state,
50
+ server_id_str,
51
+ )
52
+
53
+ if not code:
54
+ self.status = "error"
55
+ self.message = "Kein Autorisierungscode erhalten."
56
+ yield
57
+ return
58
+
59
+ # If server_id is missing, try to recover it from state
60
+ if not server_id_str and state:
61
+ with get_session_manager().session() as session:
62
+ oauth_state = (
63
+ session.execute(
64
+ select(OAuthStateEntity).where(OAuthStateEntity.state == state)
65
+ )
66
+ .scalars()
67
+ .first()
68
+ )
69
+ logger.info(
70
+ "OAuth state lookup - found: %s, provider: %s",
71
+ oauth_state is not None,
72
+ oauth_state.provider if oauth_state else "N/A",
73
+ )
74
+ if oauth_state and oauth_state.provider.startswith("mcp:"):
75
+ with contextlib.suppress(IndexError):
76
+ server_id_str = oauth_state.provider.split(":")[1]
77
+ logger.debug(
78
+ "Recovered server_id from state: %s", server_id_str
79
+ )
80
+
81
+ if not server_id_str:
82
+ self.status = "error"
83
+ self.message = "Server-ID fehlt."
84
+ yield
85
+ return
86
+
87
+ try:
88
+ server_id = int(server_id_str)
89
+ except ValueError:
90
+ self.status = "error"
91
+ self.message = "Ungültige Server-ID."
92
+ yield
93
+ return
94
+
95
+ self._code = code
96
+ self._state = state
97
+ self._server_id = server_id
98
+
99
+ # Get the server configuration
100
+ with rx.session() as session:
101
+ server = session.exec(
102
+ select(MCPServer).where(MCPServer.id == server_id)
103
+ ).first()
104
+
105
+ if not server:
106
+ self.status = "error"
107
+ self.message = "Server nicht gefunden."
108
+ yield
109
+ return
110
+
111
+ self.server_name = server.name
112
+
113
+ # Get user ID from auth state
114
+ user_id = await self._get_current_user_id()
115
+ if not user_id:
116
+ self.status = "error"
117
+ self.message = "Nicht angemeldet."
118
+ yield
119
+ return
120
+
121
+ # Exchange code for tokens - inline the logic to avoid yield from
122
+ async for result in self._do_token_exchange(
123
+ session, server, user_id, code, self._state
124
+ ):
125
+ yield result
126
+
127
+ async def _do_token_exchange(
128
+ self,
129
+ session: Session,
130
+ server: MCPServer,
131
+ user_id: int,
132
+ code: str,
133
+ state: str,
134
+ ) -> AsyncGenerator:
135
+ """Exchange the authorization code for tokens."""
136
+ redirect_uri = self._build_redirect_uri()
137
+ auth_service = MCPAuthService(redirect_uri=redirect_uri)
138
+
139
+ try:
140
+ result = await auth_service.exchange_code_for_tokens(
141
+ server, code, state=state, session=session
142
+ )
143
+
144
+ if result.error:
145
+ self.status = "error"
146
+ self.message = (
147
+ f"Token-Austausch fehlgeschlagen: {result.error_description}"
148
+ )
149
+ yield
150
+ return
151
+
152
+ # Save the token
153
+ auth_service.save_user_token(
154
+ session,
155
+ user_id,
156
+ server.id, # type: ignore
157
+ result,
158
+ )
159
+
160
+ self.status = "success"
161
+ self.message = f"Erfolgreich mit {server.name} verbunden!"
162
+ yield
163
+
164
+ # Notify parent window via localStorage (works across windows)
165
+ # Include user_id for security - prevents cross-user leakage
166
+ server_id_str = str(server.id)
167
+ logger.debug(
168
+ "OAuth success - notifying via localStorage: "
169
+ "server_id=%s, name=%s, user_id=%s",
170
+ server_id_str,
171
+ server.name,
172
+ user_id,
173
+ )
174
+ script = f"""
175
+ console.log('[OAuth] Setting localStorage for cross-window sync');
176
+ var data = JSON.stringify({{
177
+ type: 'mcp-oauth-success',
178
+ serverId: '{server_id_str}',
179
+ serverName: '{server.name}',
180
+ userId: '{user_id}',
181
+ timestamp: Date.now()
182
+ }});
183
+ localStorage.setItem('mcp-oauth-result', data);
184
+ console.log('[OAuth] localStorage set:', data);
185
+ // Also try postMessage for same-origin popups
186
+ if (window.opener) {{
187
+ window.opener.postMessage({{
188
+ type: 'mcp-oauth-success',
189
+ serverId: '{server_id_str}',
190
+ serverName: '{server.name}',
191
+ userId: '{user_id}'
192
+ }}, '*');
193
+ }}
194
+ setTimeout(function() {{ window.close(); }}, 500);
195
+ """
196
+ yield rx.call_script(script)
197
+
198
+ except Exception as e:
199
+ logger.exception("Token exchange failed")
200
+ self.status = "error"
201
+ self.message = f"Fehler: {e!s}"
202
+ yield
203
+
204
+ finally:
205
+ await auth_service.close()
206
+
207
+ async def _get_current_user_id(self) -> int | None:
208
+ """Get the current user's ID from auth state."""
209
+ auth_state = await self.get_state(UserSession)
210
+ # Verify authentication status to ensure user_id is populated from session token
211
+ await auth_state.authenticated_user
212
+ if auth_state.user_id and auth_state.user_id > 0:
213
+ return auth_state.user_id
214
+ return None
215
+
216
+ def _build_redirect_uri(self) -> str:
217
+ """Build the OAuth redirect URI from configuration.
218
+
219
+ Uses the same configuration-based URL as the authorization request
220
+ to ensure redirect_uri matches exactly.
221
+ """
222
+ return mcp_oauth_redirect_uri()