appkit-assistant 0.8.0__tar.gz → 0.9.0__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.8.0 → appkit_assistant-0.9.0}/PKG-INFO +1 -1
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/pyproject.toml +1 -1
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/models.py +45 -1
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/openai_responses_processor.py +5 -4
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/repositories.py +88 -1
- appkit_assistant-0.9.0/src/appkit_assistant/backend/system_prompt_cache.py +161 -0
- appkit_assistant-0.9.0/src/appkit_assistant/components/system_prompt_editor.py +78 -0
- appkit_assistant-0.9.0/src/appkit_assistant/state/system_prompt_state.py +181 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/.gitignore +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/README.md +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/docs/assistant.png +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/model_manager.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/ai_models.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/knowledgeai_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/openai_base.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/system_prompt.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/__init__.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/composer.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/composer_key_handler.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/mcp_server_dialogs.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/mcp_server_table.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/message.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/thread.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/threadlist.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/tools_modal.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/configuration.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/state/mcp_server_state.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/state/thread_state.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from enum import StrEnum
|
|
2
3
|
|
|
3
4
|
import reflex as rx
|
|
4
5
|
from pydantic import BaseModel
|
|
5
|
-
from sqlmodel import Field
|
|
6
|
+
from sqlmodel import Column, DateTime, Field
|
|
6
7
|
|
|
7
8
|
from appkit_commons.database.entities import EncryptedString
|
|
8
9
|
|
|
@@ -92,6 +93,14 @@ class ThreadModel(BaseModel):
|
|
|
92
93
|
ai_model: str = ""
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
class MCPAuthType(StrEnum):
|
|
97
|
+
"""Enum for MCP server authentication types."""
|
|
98
|
+
|
|
99
|
+
NONE = "none"
|
|
100
|
+
API_KEY = "api_key"
|
|
101
|
+
OAUTH_DISCOVERY = "oauth_discovery"
|
|
102
|
+
|
|
103
|
+
|
|
95
104
|
class MCPServer(rx.Model, table=True):
|
|
96
105
|
"""Model for MCP (Model Context Protocol) server configuration."""
|
|
97
106
|
|
|
@@ -103,3 +112,38 @@ class MCPServer(rx.Model, table=True):
|
|
|
103
112
|
url: str = Field(nullable=False)
|
|
104
113
|
headers: str = Field(nullable=False, sa_type=EncryptedString)
|
|
105
114
|
prompt: str = Field(default="", max_length=2000, nullable=True)
|
|
115
|
+
|
|
116
|
+
# Authentication type
|
|
117
|
+
auth_type: str = Field(default=MCPAuthType.NONE, nullable=False)
|
|
118
|
+
|
|
119
|
+
# Optional discovery URL override
|
|
120
|
+
discovery_url: str | None = Field(default=None, nullable=True)
|
|
121
|
+
|
|
122
|
+
# Cached OAuth/Discovery metadata (read-only for user mostly)
|
|
123
|
+
oauth_issuer: str | None = Field(default=None, nullable=True)
|
|
124
|
+
oauth_authorize_url: str | None = Field(default=None, nullable=True)
|
|
125
|
+
oauth_token_url: str | None = Field(default=None, nullable=True)
|
|
126
|
+
oauth_scopes: str | None = Field(
|
|
127
|
+
default=None, nullable=True
|
|
128
|
+
) # Space separated scopes
|
|
129
|
+
|
|
130
|
+
# Timestamp when discovery was last successfully run
|
|
131
|
+
oauth_discovered_at: datetime | None = Field(
|
|
132
|
+
default=None, sa_column=Column(DateTime(timezone=True), nullable=True)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class SystemPrompt(rx.Model, table=True):
|
|
137
|
+
"""Model for system prompt versioning and management.
|
|
138
|
+
|
|
139
|
+
Each save creates a new immutable version. Supports up to 20,000 characters.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
__tablename__ = "system_prompt"
|
|
143
|
+
|
|
144
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
145
|
+
name: str = Field(max_length=200, nullable=False)
|
|
146
|
+
prompt: str = Field(max_length=20000, nullable=False)
|
|
147
|
+
version: int = Field(nullable=False)
|
|
148
|
+
user_id: int = Field(nullable=False)
|
|
149
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
@@ -12,7 +12,7 @@ from appkit_assistant.backend.models import (
|
|
|
12
12
|
MessageType,
|
|
13
13
|
)
|
|
14
14
|
from appkit_assistant.backend.processors.openai_base import BaseOpenAIProcessor
|
|
15
|
-
from appkit_assistant.backend.
|
|
15
|
+
from appkit_assistant.backend.system_prompt_cache import get_system_prompt
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -404,7 +404,7 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
|
|
|
404
404
|
)
|
|
405
405
|
|
|
406
406
|
# Convert messages to responses format with system message
|
|
407
|
-
input_messages = self._convert_messages_to_responses_format(
|
|
407
|
+
input_messages = await self._convert_messages_to_responses_format(
|
|
408
408
|
messages, mcp_prompt=mcp_prompt
|
|
409
409
|
)
|
|
410
410
|
|
|
@@ -453,7 +453,7 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
|
|
|
453
453
|
prompt_string = "\n".join(prompts) if prompts else ""
|
|
454
454
|
return tools, prompt_string
|
|
455
455
|
|
|
456
|
-
def _convert_messages_to_responses_format(
|
|
456
|
+
async def _convert_messages_to_responses_format(
|
|
457
457
|
self, messages: list[Message], mcp_prompt: str = ""
|
|
458
458
|
) -> list[dict[str, Any]]:
|
|
459
459
|
"""Convert messages to the responses API input format.
|
|
@@ -471,7 +471,8 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
|
|
|
471
471
|
else:
|
|
472
472
|
mcp_prompt = ""
|
|
473
473
|
|
|
474
|
-
|
|
474
|
+
system_prompt_template = await get_system_prompt()
|
|
475
|
+
system_text = system_prompt_template.format(mcp_prompts=mcp_prompt)
|
|
475
476
|
input_messages.append(
|
|
476
477
|
{
|
|
477
478
|
"role": "system",
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/repositories.py
RENAMED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""Repository for MCP server data access operations."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
from datetime import UTC, datetime
|
|
4
5
|
|
|
5
6
|
import reflex as rx
|
|
6
7
|
|
|
7
|
-
from appkit_assistant.backend.models import MCPServer
|
|
8
|
+
from appkit_assistant.backend.models import MCPServer, SystemPrompt
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
@@ -94,3 +95,89 @@ class MCPServerRepository:
|
|
|
94
95
|
return True
|
|
95
96
|
logger.warning("MCP server with ID %s not found for deletion", server_id)
|
|
96
97
|
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SystemPromptRepository:
|
|
101
|
+
"""Repository class for system prompt database operations.
|
|
102
|
+
|
|
103
|
+
Implements append-only versioning with full CRUD capabilities.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
async def get_all() -> list[SystemPrompt]:
|
|
108
|
+
"""Retrieve all system prompt versions ordered by version descending."""
|
|
109
|
+
async with rx.asession() as session:
|
|
110
|
+
result = await session.exec(
|
|
111
|
+
SystemPrompt.select().order_by(SystemPrompt.version.desc())
|
|
112
|
+
)
|
|
113
|
+
return result.all()
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
async def get_latest() -> SystemPrompt | None:
|
|
117
|
+
"""Retrieve the latest system prompt version."""
|
|
118
|
+
async with rx.asession() as session:
|
|
119
|
+
result = await session.exec(
|
|
120
|
+
SystemPrompt.select().order_by(SystemPrompt.version.desc()).limit(1)
|
|
121
|
+
)
|
|
122
|
+
return result.first()
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
async def get_by_id(prompt_id: int) -> SystemPrompt | None:
|
|
126
|
+
"""Retrieve a system prompt by ID."""
|
|
127
|
+
async with rx.asession() as session:
|
|
128
|
+
result = await session.exec(
|
|
129
|
+
SystemPrompt.select().where(SystemPrompt.id == prompt_id)
|
|
130
|
+
)
|
|
131
|
+
return result.first()
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
async def create(prompt: str, user_id: int) -> SystemPrompt:
|
|
135
|
+
"""Neue System Prompt Version anlegen.
|
|
136
|
+
|
|
137
|
+
Version ist fortlaufende Ganzzahl, beginnend bei 1.
|
|
138
|
+
"""
|
|
139
|
+
async with rx.asession() as session:
|
|
140
|
+
result = await session.exec(
|
|
141
|
+
SystemPrompt.select().order_by(SystemPrompt.version.desc()).limit(1)
|
|
142
|
+
)
|
|
143
|
+
latest = result.first()
|
|
144
|
+
next_version = (latest.version + 1) if latest else 1
|
|
145
|
+
|
|
146
|
+
name = f"Version {next_version}"
|
|
147
|
+
|
|
148
|
+
system_prompt = SystemPrompt(
|
|
149
|
+
name=name,
|
|
150
|
+
prompt=prompt,
|
|
151
|
+
version=next_version,
|
|
152
|
+
user_id=user_id,
|
|
153
|
+
created_at=datetime.now(UTC),
|
|
154
|
+
)
|
|
155
|
+
session.add(system_prompt)
|
|
156
|
+
await session.commit()
|
|
157
|
+
await session.refresh(system_prompt)
|
|
158
|
+
|
|
159
|
+
logger.info(
|
|
160
|
+
"Created system prompt version %s for user %s",
|
|
161
|
+
next_version,
|
|
162
|
+
user_id,
|
|
163
|
+
)
|
|
164
|
+
return system_prompt
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
async def delete(prompt_id: int) -> bool:
|
|
168
|
+
"""Delete a system prompt version by ID."""
|
|
169
|
+
async with rx.asession() as session:
|
|
170
|
+
result = await session.exec(
|
|
171
|
+
SystemPrompt.select().where(SystemPrompt.id == prompt_id)
|
|
172
|
+
)
|
|
173
|
+
prompt = result.first()
|
|
174
|
+
if prompt:
|
|
175
|
+
await session.delete(prompt)
|
|
176
|
+
await session.commit()
|
|
177
|
+
logger.info("Deleted system prompt version: %s", prompt.version)
|
|
178
|
+
return True
|
|
179
|
+
logger.warning(
|
|
180
|
+
"System prompt with ID %s not found for deletion",
|
|
181
|
+
prompt_id,
|
|
182
|
+
)
|
|
183
|
+
return False
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
|
+
from typing import Final
|
|
5
|
+
|
|
6
|
+
from appkit_assistant.backend.repositories import SystemPromptRepository
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
# Cache TTL in seconds (default: 5 minutes)
|
|
11
|
+
CACHE_TTL_SECONDS: Final[int] = 300
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SystemPromptCache:
|
|
15
|
+
"""Singleton cache for system prompt with TTL-based invalidation.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- Lazy loading on first access
|
|
19
|
+
- Automatic cache invalidation after TTL expires
|
|
20
|
+
- Thread-safe with asyncio lock
|
|
21
|
+
- Manual invalidation support for immediate updates
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_instance: "SystemPromptCache | None" = None
|
|
25
|
+
_lock: asyncio.Lock = asyncio.Lock()
|
|
26
|
+
|
|
27
|
+
def __new__(cls) -> "SystemPromptCache":
|
|
28
|
+
"""Ensure singleton pattern."""
|
|
29
|
+
if cls._instance is None:
|
|
30
|
+
cls._instance = super().__new__(cls)
|
|
31
|
+
cls._instance._initialized = False # noqa: SLF001
|
|
32
|
+
return cls._instance
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
"""Initialize cache state (only once due to singleton)."""
|
|
36
|
+
if self._initialized:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
self._cached_prompt: str | None = None
|
|
40
|
+
self._cached_version: int | None = None
|
|
41
|
+
self._cache_timestamp: datetime | None = None
|
|
42
|
+
self._ttl_seconds: int = CACHE_TTL_SECONDS
|
|
43
|
+
self._initialized = True
|
|
44
|
+
|
|
45
|
+
logger.info(
|
|
46
|
+
"SystemPromptCache initialized with TTL=%d seconds",
|
|
47
|
+
self._ttl_seconds,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _is_cache_valid(self) -> bool:
|
|
51
|
+
"""Check if cached prompt is still valid based on TTL."""
|
|
52
|
+
if self._cached_prompt is None or self._cache_timestamp is None:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
elapsed = datetime.now(UTC) - self._cache_timestamp
|
|
56
|
+
is_valid = elapsed < timedelta(seconds=self._ttl_seconds)
|
|
57
|
+
|
|
58
|
+
if not is_valid:
|
|
59
|
+
logger.debug("Cache expired after %s seconds", elapsed.total_seconds())
|
|
60
|
+
|
|
61
|
+
return is_valid
|
|
62
|
+
|
|
63
|
+
async def get_prompt(self) -> str:
|
|
64
|
+
"""Get the latest system prompt (from cache or database).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The current system prompt text.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: If no system prompt exists in database.
|
|
71
|
+
"""
|
|
72
|
+
async with self._lock:
|
|
73
|
+
if self._is_cache_valid():
|
|
74
|
+
logger.debug(
|
|
75
|
+
"Cache hit: version=%d, age=%s seconds",
|
|
76
|
+
self._cached_version,
|
|
77
|
+
(datetime.now(UTC) - self._cache_timestamp).total_seconds(),
|
|
78
|
+
)
|
|
79
|
+
return self._cached_prompt
|
|
80
|
+
|
|
81
|
+
# Cache miss or expired - fetch from database
|
|
82
|
+
logger.info("Cache miss - fetching latest prompt from database")
|
|
83
|
+
|
|
84
|
+
latest_prompt = await SystemPromptRepository.get_latest()
|
|
85
|
+
|
|
86
|
+
if latest_prompt is None:
|
|
87
|
+
msg = "No system prompt found in database"
|
|
88
|
+
logger.error(msg)
|
|
89
|
+
raise ValueError(msg)
|
|
90
|
+
|
|
91
|
+
self._cached_prompt = latest_prompt.prompt
|
|
92
|
+
self._cached_version = latest_prompt.version
|
|
93
|
+
self._cache_timestamp = datetime.now(UTC)
|
|
94
|
+
|
|
95
|
+
logger.info(
|
|
96
|
+
"Cached prompt version %d (%d characters)",
|
|
97
|
+
self._cached_version,
|
|
98
|
+
len(self._cached_prompt),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return self._cached_prompt
|
|
102
|
+
|
|
103
|
+
async def invalidate(self) -> None:
|
|
104
|
+
"""Manually invalidate the cache.
|
|
105
|
+
|
|
106
|
+
Use this when a new prompt version is created to force
|
|
107
|
+
immediate reload on next access.
|
|
108
|
+
"""
|
|
109
|
+
async with self._lock:
|
|
110
|
+
if self._cached_prompt is not None:
|
|
111
|
+
logger.info(
|
|
112
|
+
"Cache invalidated (was version %d)",
|
|
113
|
+
self._cached_version,
|
|
114
|
+
)
|
|
115
|
+
self._cached_prompt = None
|
|
116
|
+
self._cached_version = None
|
|
117
|
+
self._cache_timestamp = None
|
|
118
|
+
else:
|
|
119
|
+
logger.debug("Cache invalidation called but cache was empty")
|
|
120
|
+
|
|
121
|
+
def set_ttl(self, seconds: int) -> None:
|
|
122
|
+
"""Update cache TTL.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
seconds: New TTL in seconds.
|
|
126
|
+
"""
|
|
127
|
+
self._ttl_seconds = seconds
|
|
128
|
+
logger.info("Cache TTL updated to %d seconds", seconds)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def is_cached(self) -> bool:
|
|
132
|
+
"""Check if prompt is currently cached and valid."""
|
|
133
|
+
return self._is_cache_valid()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def cached_version(self) -> int | None:
|
|
137
|
+
"""Get the currently cached prompt version (if any)."""
|
|
138
|
+
return self._cached_version if self._is_cache_valid() else None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# Global cache instance
|
|
142
|
+
_prompt_cache = SystemPromptCache()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def get_system_prompt() -> str:
|
|
146
|
+
"""Convenience function to get the current system prompt.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The current system prompt text.
|
|
150
|
+
"""
|
|
151
|
+
return await _prompt_cache.get_prompt()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def invalidate_prompt_cache() -> None:
|
|
155
|
+
"""Convenience function to invalidate the prompt cache."""
|
|
156
|
+
await _prompt_cache.invalidate()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_cache_instance() -> SystemPromptCache:
|
|
160
|
+
"""Get the global cache instance for advanced usage."""
|
|
161
|
+
return _prompt_cache
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
# Trigger to force textarea update when selecting a version
|
|
29
|
+
textarea_key: int = 0
|
|
30
|
+
|
|
31
|
+
async def load_versions(self) -> None:
|
|
32
|
+
"""Alle System Prompt Versionen laden."""
|
|
33
|
+
self.is_loading = True
|
|
34
|
+
self.error_message = ""
|
|
35
|
+
try:
|
|
36
|
+
prompts = await SystemPromptRepository.get_all()
|
|
37
|
+
self.versions = [
|
|
38
|
+
{
|
|
39
|
+
"value": str(p.version),
|
|
40
|
+
"label": (
|
|
41
|
+
f"Version {p.version} - "
|
|
42
|
+
f"{p.created_at.strftime('%d.%m.%Y %H:%M')}"
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
for p in prompts
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Populate map for fast switching
|
|
49
|
+
self.prompt_map = {str(p.version): p.prompt for p in prompts}
|
|
50
|
+
|
|
51
|
+
if prompts:
|
|
52
|
+
latest = prompts[0]
|
|
53
|
+
# Automatically select the latest version
|
|
54
|
+
self.selected_version_id = latest.version
|
|
55
|
+
|
|
56
|
+
if not self.current_prompt:
|
|
57
|
+
self.current_prompt = latest.prompt
|
|
58
|
+
self.last_saved_prompt = latest.prompt
|
|
59
|
+
else:
|
|
60
|
+
self.last_saved_prompt = latest.prompt
|
|
61
|
+
else:
|
|
62
|
+
self.selected_version_id = 0
|
|
63
|
+
if not self.current_prompt:
|
|
64
|
+
self.current_prompt = ""
|
|
65
|
+
self.last_saved_prompt = self.current_prompt
|
|
66
|
+
|
|
67
|
+
# Zähler initial setzen
|
|
68
|
+
self.char_count = len(self.current_prompt)
|
|
69
|
+
|
|
70
|
+
logger.info("Loaded %s system prompt versions", len(self.versions))
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
self.error_message = f"Fehler beim Laden: {exc!s}"
|
|
73
|
+
logger.exception("Failed to load system prompt versions")
|
|
74
|
+
finally:
|
|
75
|
+
self.is_loading = False
|
|
76
|
+
|
|
77
|
+
async def save_current(self) -> AsyncGenerator[Any, Any]:
|
|
78
|
+
if self.current_prompt == self.last_saved_prompt:
|
|
79
|
+
yield rx.toast.info("Es wurden keine Änderungen erkannt.")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if not self.current_prompt.strip():
|
|
83
|
+
self.error_message = "Prompt darf nicht leer sein."
|
|
84
|
+
yield rx.toast.error("Prompt darf nicht leer sein.")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if len(self.current_prompt) > MAX_PROMPT_LENGTH:
|
|
88
|
+
self.error_message = "Prompt darf maximal 20.000 Zeichen enthalten."
|
|
89
|
+
yield rx.toast.error("Prompt ist zu lang (max. 20.000 Zeichen).")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
self.is_loading = True
|
|
93
|
+
self.error_message = ""
|
|
94
|
+
try:
|
|
95
|
+
user_session: UserSession = await self.get_state(UserSession)
|
|
96
|
+
user_id = user_session.user_id
|
|
97
|
+
|
|
98
|
+
await SystemPromptRepository.create(
|
|
99
|
+
prompt=self.current_prompt,
|
|
100
|
+
user_id=user_id,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.last_saved_prompt = self.current_prompt
|
|
104
|
+
|
|
105
|
+
# Invalidate cache to force reload of new prompt version
|
|
106
|
+
await invalidate_prompt_cache()
|
|
107
|
+
logger.info("System prompt cache invalidated after save")
|
|
108
|
+
|
|
109
|
+
await self.load_versions()
|
|
110
|
+
|
|
111
|
+
yield rx.toast.success("Neue Version erfolgreich gespeichert.")
|
|
112
|
+
logger.info("Saved new system prompt version by user %s", user_id)
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
self.error_message = f"Fehler beim Speichern: {exc!s}"
|
|
115
|
+
logger.exception("Failed to save system prompt")
|
|
116
|
+
yield rx.toast.error(f"Fehler: {exc!s}")
|
|
117
|
+
finally:
|
|
118
|
+
self.is_loading = False
|
|
119
|
+
|
|
120
|
+
async def delete_version(self) -> AsyncGenerator[Any, Any]:
|
|
121
|
+
if not self.selected_version_id:
|
|
122
|
+
self.error_message = "Keine Version ausgewählt."
|
|
123
|
+
yield rx.toast.error("Bitte zuerst eine Version auswählen.")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
self.is_loading = True
|
|
127
|
+
self.error_message = ""
|
|
128
|
+
try:
|
|
129
|
+
success = await SystemPromptRepository.delete(self.selected_version_id)
|
|
130
|
+
if success:
|
|
131
|
+
self.selected_version_id = 0
|
|
132
|
+
|
|
133
|
+
# Invalidate cache since latest version might have changed
|
|
134
|
+
await invalidate_prompt_cache()
|
|
135
|
+
logger.info("System prompt cache invalidated after deletion")
|
|
136
|
+
|
|
137
|
+
await self.load_versions()
|
|
138
|
+
yield rx.toast.success("Version erfolgreich gelöscht.")
|
|
139
|
+
else:
|
|
140
|
+
self.error_message = "Version nicht gefunden."
|
|
141
|
+
yield rx.toast.error("Version nicht gefunden.")
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
self.error_message = f"Fehler beim Löschen: {exc!s}"
|
|
144
|
+
logger.exception("Failed to delete version")
|
|
145
|
+
yield rx.toast.error(f"Fehler: {exc!s}")
|
|
146
|
+
finally:
|
|
147
|
+
self.is_loading = False
|
|
148
|
+
|
|
149
|
+
def set_current_prompt(self, value: str) -> None:
|
|
150
|
+
"""Update current prompt text and char count.
|
|
151
|
+
|
|
152
|
+
This is called on every keystroke but doesn't cause cursor jumping
|
|
153
|
+
because we use default_value in the textarea component.
|
|
154
|
+
"""
|
|
155
|
+
self.current_prompt = value
|
|
156
|
+
self.char_count = len(value)
|
|
157
|
+
|
|
158
|
+
def set_selected_version(self, value: str | None) -> None:
|
|
159
|
+
"""Handle version selection and load corresponding prompt.
|
|
160
|
+
|
|
161
|
+
When a version is selected, we update the prompt content and force
|
|
162
|
+
the textarea to re-render by changing its key.
|
|
163
|
+
"""
|
|
164
|
+
if value is None or value == "":
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
self.selected_version_id = int(value)
|
|
168
|
+
|
|
169
|
+
# Load the prompt for the selected version
|
|
170
|
+
if value in self.prompt_map:
|
|
171
|
+
self.current_prompt = self.prompt_map[value]
|
|
172
|
+
self.char_count = len(self.current_prompt)
|
|
173
|
+
# Force textarea to re-render with new content
|
|
174
|
+
self.textarea_key += 1
|
|
175
|
+
|
|
176
|
+
@rx.var
|
|
177
|
+
def selected_version_str(self) -> str:
|
|
178
|
+
"""Return selected version as string for the select component."""
|
|
179
|
+
if self.selected_version_id == 0:
|
|
180
|
+
return ""
|
|
181
|
+
return str(self.selected_version_id)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/model_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/backend/system_prompt.py
RENAMED
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/__init__.py
RENAMED
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/composer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/message.py
RENAMED
|
File without changes
|
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/threadlist.py
RENAMED
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/components/tools_modal.py
RENAMED
|
File without changes
|
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/state/mcp_server_state.py
RENAMED
|
File without changes
|
{appkit_assistant-0.8.0 → appkit_assistant-0.9.0}/src/appkit_assistant/state/thread_state.py
RENAMED
|
File without changes
|