appkit-assistant 0.8.0__tar.gz → 0.10.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.10.0}/PKG-INFO +2 -2
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/pyproject.toml +8 -8
- appkit_assistant-0.10.0/src/appkit_assistant/backend/models.py +196 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/openai_responses_processor.py +16 -11
- appkit_assistant-0.10.0/src/appkit_assistant/backend/repositories.py +323 -0
- appkit_assistant-0.10.0/src/appkit_assistant/backend/system_prompt_cache.py +161 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/__init__.py +2 -4
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/mcp_server_dialogs.py +7 -2
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/message.py +3 -3
- appkit_assistant-0.10.0/src/appkit_assistant/components/system_prompt_editor.py +78 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/thread.py +8 -16
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/threadlist.py +42 -29
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/tools_modal.py +1 -1
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/configuration.py +1 -0
- appkit_assistant-0.10.0/src/appkit_assistant/state/system_prompt_state.py +179 -0
- appkit_assistant-0.10.0/src/appkit_assistant/state/thread_list_state.py +271 -0
- appkit_assistant-0.10.0/src/appkit_assistant/state/thread_state.py +791 -0
- appkit_assistant-0.8.0/src/appkit_assistant/backend/models.py +0 -105
- appkit_assistant-0.8.0/src/appkit_assistant/backend/repositories.py +0 -96
- appkit_assistant-0.8.0/src/appkit_assistant/state/thread_state.py +0 -874
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/.gitignore +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/README.md +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/docs/assistant.png +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/model_manager.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/ai_models.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/knowledgeai_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/openai_base.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/backend/system_prompt.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/composer.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/composer_key_handler.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/components/mcp_server_table.py +0 -0
- {appkit_assistant-0.8.0 → appkit_assistant-0.10.0}/src/appkit_assistant/state/mcp_server_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appkit-assistant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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
|
|
@@ -20,7 +20,7 @@ Requires-Dist: appkit-commons
|
|
|
20
20
|
Requires-Dist: appkit-mantine
|
|
21
21
|
Requires-Dist: appkit-ui
|
|
22
22
|
Requires-Dist: openai>=2.3.0
|
|
23
|
-
Requires-Dist: reflex>=0.8.
|
|
23
|
+
Requires-Dist: reflex>=0.8.22
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
26
26
|
# appkit-assistant
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
|
+
dependencies = [
|
|
3
|
+
"appkit-commons",
|
|
4
|
+
"appkit-mantine",
|
|
5
|
+
"appkit-ui",
|
|
6
|
+
"openai>=2.3.0",
|
|
7
|
+
"reflex>=0.8.22",
|
|
8
|
+
]
|
|
2
9
|
name = "appkit-assistant"
|
|
3
|
-
version = "0.
|
|
10
|
+
version = "0.10.0"
|
|
4
11
|
description = "Add your description here"
|
|
5
12
|
readme = "README.md"
|
|
6
13
|
authors = [{ name = "Jens Rehpöhler" }]
|
|
@@ -21,13 +28,6 @@ classifiers = [
|
|
|
21
28
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
29
|
"Topic :: Software Development :: User Interfaces",
|
|
23
30
|
]
|
|
24
|
-
dependencies = [
|
|
25
|
-
"appkit-commons",
|
|
26
|
-
"appkit-mantine",
|
|
27
|
-
"appkit-ui",
|
|
28
|
-
"openai>=2.3.0",
|
|
29
|
-
"reflex>=0.8.20",
|
|
30
|
-
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
33
|
Homepage = "https://github.com/jenreh/appkit"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import reflex as rx
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from sqlalchemy.sql import func
|
|
9
|
+
from sqlmodel import Column, DateTime, Field
|
|
10
|
+
|
|
11
|
+
from appkit_commons.database.configuration import DatabaseConfig
|
|
12
|
+
from appkit_commons.database.entities import EncryptedString
|
|
13
|
+
from appkit_commons.registry import service_registry
|
|
14
|
+
|
|
15
|
+
db_config = service_registry().get(DatabaseConfig)
|
|
16
|
+
SECRET_VALUE = db_config.encryption_key.get_secret_value()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EncryptedJSON(EncryptedString):
|
|
20
|
+
"""Custom type for storing encrypted JSON data."""
|
|
21
|
+
|
|
22
|
+
def process_bind_param(self, value: Any, dialect: Any) -> str | None:
|
|
23
|
+
if value is not None:
|
|
24
|
+
value = json.dumps(value)
|
|
25
|
+
return super().process_bind_param(value, dialect)
|
|
26
|
+
|
|
27
|
+
def process_result_value(self, value: Any, dialect: Any) -> Any | None:
|
|
28
|
+
value = super().process_result_value(value, dialect)
|
|
29
|
+
if value is not None:
|
|
30
|
+
return json.loads(value)
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ChunkType(StrEnum):
|
|
35
|
+
"""Enum for chunk types."""
|
|
36
|
+
|
|
37
|
+
TEXT = "text" # default
|
|
38
|
+
ANNOTATION = "annotation" # for text annotations
|
|
39
|
+
IMAGE = "image"
|
|
40
|
+
IMAGE_PARTIAL = "image_partial" # for streaming image generation
|
|
41
|
+
THINKING = "thinking" # when the model is "thinking" / reasoning
|
|
42
|
+
THINKING_RESULT = "thinking_result" # when the "thinking" is done
|
|
43
|
+
ACTION = "action" # when the user needs to take action
|
|
44
|
+
TOOL_RESULT = "tool_result" # result from a tool
|
|
45
|
+
TOOL_CALL = "tool_call" # calling a tool
|
|
46
|
+
COMPLETION = "completion" # when response generation is complete
|
|
47
|
+
ERROR = "error" # when an error occurs
|
|
48
|
+
LIFECYCLE = "lifecycle"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Chunk(BaseModel):
|
|
52
|
+
"""Model for text chunks."""
|
|
53
|
+
|
|
54
|
+
type: ChunkType
|
|
55
|
+
text: str
|
|
56
|
+
chunk_metadata: dict[str, str] = {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ThreadStatus(StrEnum):
|
|
60
|
+
"""Enum for thread status."""
|
|
61
|
+
|
|
62
|
+
NEW = "new"
|
|
63
|
+
ACTIVE = "active"
|
|
64
|
+
IDLE = "idle"
|
|
65
|
+
WAITING = "waiting"
|
|
66
|
+
ERROR = "error"
|
|
67
|
+
DELETED = "deleted"
|
|
68
|
+
ARCHIVED = "archived"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MessageType(StrEnum):
|
|
72
|
+
"""Enum for message types."""
|
|
73
|
+
|
|
74
|
+
HUMAN = "human"
|
|
75
|
+
SYSTEM = "system"
|
|
76
|
+
ASSISTANT = "assistant"
|
|
77
|
+
TOOL_USE = "tool_use"
|
|
78
|
+
ERROR = "error"
|
|
79
|
+
INFO = "info"
|
|
80
|
+
WARNING = "warning"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Message(BaseModel):
|
|
84
|
+
text: str
|
|
85
|
+
editable: bool = False
|
|
86
|
+
type: MessageType
|
|
87
|
+
done: bool = False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AIModel(BaseModel):
|
|
91
|
+
id: str
|
|
92
|
+
text: str
|
|
93
|
+
icon: str = "codesandbox"
|
|
94
|
+
stream: bool = False
|
|
95
|
+
tenant_key: str = ""
|
|
96
|
+
project_id: int = 0
|
|
97
|
+
model: str = "default"
|
|
98
|
+
temperature: float = 0.05
|
|
99
|
+
supports_tools: bool = False
|
|
100
|
+
supports_attachments: bool = False
|
|
101
|
+
keywords: list[str] = []
|
|
102
|
+
disabled: bool = False
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Suggestion(BaseModel):
|
|
106
|
+
prompt: str
|
|
107
|
+
icon: str = ""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class ThreadModel(BaseModel):
|
|
111
|
+
thread_id: str
|
|
112
|
+
title: str = ""
|
|
113
|
+
active: bool = False
|
|
114
|
+
state: ThreadStatus = ThreadStatus.NEW
|
|
115
|
+
prompt: str | None = ""
|
|
116
|
+
messages: list[Message] = []
|
|
117
|
+
ai_model: str = ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class MCPAuthType(StrEnum):
|
|
121
|
+
"""Enum for MCP server authentication types."""
|
|
122
|
+
|
|
123
|
+
NONE = "none"
|
|
124
|
+
API_KEY = "api_key"
|
|
125
|
+
OAUTH_DISCOVERY = "oauth_discovery"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class MCPServer(rx.Model, table=True):
|
|
129
|
+
"""Model for MCP (Model Context Protocol) server configuration."""
|
|
130
|
+
|
|
131
|
+
__tablename__ = "assistant_mcp_servers"
|
|
132
|
+
|
|
133
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
134
|
+
name: str = Field(unique=True, max_length=100, nullable=False)
|
|
135
|
+
description: str = Field(default="", max_length=255, nullable=True)
|
|
136
|
+
url: str = Field(nullable=False)
|
|
137
|
+
headers: str = Field(nullable=False, sa_type=EncryptedString)
|
|
138
|
+
prompt: str = Field(default="", max_length=2000, nullable=True)
|
|
139
|
+
|
|
140
|
+
# Authentication type
|
|
141
|
+
auth_type: str = Field(default=MCPAuthType.NONE, nullable=False)
|
|
142
|
+
|
|
143
|
+
# Optional discovery URL override
|
|
144
|
+
discovery_url: str | None = Field(default=None, nullable=True)
|
|
145
|
+
|
|
146
|
+
# Cached OAuth/Discovery metadata (read-only for user mostly)
|
|
147
|
+
oauth_issuer: str | None = Field(default=None, nullable=True)
|
|
148
|
+
oauth_authorize_url: str | None = Field(default=None, nullable=True)
|
|
149
|
+
oauth_token_url: str | None = Field(default=None, nullable=True)
|
|
150
|
+
oauth_scopes: str | None = Field(
|
|
151
|
+
default=None, nullable=True
|
|
152
|
+
) # Space separated scopes
|
|
153
|
+
|
|
154
|
+
# Timestamp when discovery was last successfully run
|
|
155
|
+
oauth_discovered_at: datetime | None = Field(
|
|
156
|
+
default=None, sa_column=Column(DateTime(timezone=True), nullable=True)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class SystemPrompt(rx.Model, table=True):
|
|
161
|
+
"""Model for system prompt versioning and management.
|
|
162
|
+
|
|
163
|
+
Each save creates a new immutable version. Supports up to 20,000 characters.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
__tablename__ = "assistant_system_prompt"
|
|
167
|
+
|
|
168
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
169
|
+
name: str = Field(max_length=200, nullable=False)
|
|
170
|
+
prompt: str = Field(max_length=20000, nullable=False)
|
|
171
|
+
version: int = Field(nullable=False)
|
|
172
|
+
user_id: int = Field(nullable=False)
|
|
173
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AssistantThread(rx.Model, table=True):
|
|
177
|
+
"""Model for storing chat threads in the database."""
|
|
178
|
+
|
|
179
|
+
__tablename__ = "assistant_thread"
|
|
180
|
+
|
|
181
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
182
|
+
thread_id: str = Field(unique=True, index=True, nullable=False)
|
|
183
|
+
user_id: int = Field(index=True, nullable=False)
|
|
184
|
+
title: str = Field(default="", nullable=False)
|
|
185
|
+
state: str = Field(default=ThreadStatus.NEW, nullable=False)
|
|
186
|
+
ai_model: str = Field(default="", nullable=False)
|
|
187
|
+
active: bool = Field(default=False, nullable=False)
|
|
188
|
+
messages: list[dict[str, Any]] = Field(default=[], sa_column=Column(EncryptedJSON))
|
|
189
|
+
created_at: datetime = Field(
|
|
190
|
+
default_factory=lambda: datetime.now(UTC),
|
|
191
|
+
sa_column=Column(DateTime(timezone=True)),
|
|
192
|
+
)
|
|
193
|
+
updated_at: datetime = Field(
|
|
194
|
+
default_factory=lambda: datetime.now(UTC),
|
|
195
|
+
sa_column=Column(DateTime(timezone=True), onupdate=func.now()),
|
|
196
|
+
)
|
|
@@ -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,8 +453,11 @@ 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(
|
|
457
|
-
self,
|
|
456
|
+
async def _convert_messages_to_responses_format(
|
|
457
|
+
self,
|
|
458
|
+
messages: list[Message],
|
|
459
|
+
mcp_prompt: str = "",
|
|
460
|
+
use_system_prompt: bool = True,
|
|
458
461
|
) -> list[dict[str, Any]]:
|
|
459
462
|
"""Convert messages to the responses API input format.
|
|
460
463
|
|
|
@@ -471,13 +474,15 @@ class OpenAIResponsesProcessor(BaseOpenAIProcessor):
|
|
|
471
474
|
else:
|
|
472
475
|
mcp_prompt = ""
|
|
473
476
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
477
|
+
if use_system_prompt:
|
|
478
|
+
system_prompt_template = await get_system_prompt()
|
|
479
|
+
system_text = system_prompt_template.format(mcp_prompts=mcp_prompt)
|
|
480
|
+
input_messages.append(
|
|
481
|
+
{
|
|
482
|
+
"role": "system",
|
|
483
|
+
"content": [{"type": "input_text", "text": system_text}],
|
|
484
|
+
}
|
|
485
|
+
)
|
|
481
486
|
|
|
482
487
|
# Add conversation messages
|
|
483
488
|
for msg in messages:
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Repository for MCP server data access operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
|
|
6
|
+
import reflex as rx
|
|
7
|
+
from sqlalchemy.orm import defer
|
|
8
|
+
|
|
9
|
+
from appkit_assistant.backend.models import (
|
|
10
|
+
AssistantThread,
|
|
11
|
+
MCPServer,
|
|
12
|
+
Message,
|
|
13
|
+
SystemPrompt,
|
|
14
|
+
ThreadModel,
|
|
15
|
+
ThreadStatus,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MCPServerRepository:
|
|
22
|
+
"""Repository class for MCP server database operations."""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
async def get_all() -> list[MCPServer]:
|
|
26
|
+
"""Retrieve all MCP servers ordered by name."""
|
|
27
|
+
async with rx.asession() as session:
|
|
28
|
+
result = await session.exec(MCPServer.select().order_by(MCPServer.name))
|
|
29
|
+
return result.all()
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
async def get_by_id(server_id: int) -> MCPServer | None:
|
|
33
|
+
"""Retrieve an MCP server by ID."""
|
|
34
|
+
async with rx.asession() as session:
|
|
35
|
+
result = await session.exec(
|
|
36
|
+
MCPServer.select().where(MCPServer.id == server_id)
|
|
37
|
+
)
|
|
38
|
+
return result.first()
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
async def create(
|
|
42
|
+
name: str,
|
|
43
|
+
url: str,
|
|
44
|
+
headers: str,
|
|
45
|
+
description: str | None = None,
|
|
46
|
+
prompt: str | None = None,
|
|
47
|
+
) -> MCPServer:
|
|
48
|
+
"""Create a new MCP server."""
|
|
49
|
+
async with rx.asession() as session:
|
|
50
|
+
server = MCPServer(
|
|
51
|
+
name=name,
|
|
52
|
+
url=url,
|
|
53
|
+
headers=headers,
|
|
54
|
+
description=description,
|
|
55
|
+
prompt=prompt,
|
|
56
|
+
)
|
|
57
|
+
session.add(server)
|
|
58
|
+
await session.commit()
|
|
59
|
+
await session.refresh(server)
|
|
60
|
+
logger.debug("Created MCP server: %s", name)
|
|
61
|
+
return server
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
async def update(
|
|
65
|
+
server_id: int,
|
|
66
|
+
name: str,
|
|
67
|
+
url: str,
|
|
68
|
+
headers: str,
|
|
69
|
+
description: str | None = None,
|
|
70
|
+
prompt: str | None = None,
|
|
71
|
+
) -> MCPServer | None:
|
|
72
|
+
"""Update an existing MCP server."""
|
|
73
|
+
async with rx.asession() as session:
|
|
74
|
+
result = await session.exec(
|
|
75
|
+
MCPServer.select().where(MCPServer.id == server_id)
|
|
76
|
+
)
|
|
77
|
+
server = result.first()
|
|
78
|
+
if server:
|
|
79
|
+
server.name = name
|
|
80
|
+
server.url = url
|
|
81
|
+
server.headers = headers
|
|
82
|
+
server.description = description
|
|
83
|
+
server.prompt = prompt
|
|
84
|
+
await session.commit()
|
|
85
|
+
await session.refresh(server)
|
|
86
|
+
logger.debug("Updated MCP server: %s", name)
|
|
87
|
+
return server
|
|
88
|
+
logger.warning("MCP server with ID %s not found for update", server_id)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
async def delete(server_id: int) -> bool:
|
|
93
|
+
"""Delete an MCP server by ID."""
|
|
94
|
+
async with rx.asession() as session:
|
|
95
|
+
result = await session.exec(
|
|
96
|
+
MCPServer.select().where(MCPServer.id == server_id)
|
|
97
|
+
)
|
|
98
|
+
server = result.first()
|
|
99
|
+
if server:
|
|
100
|
+
await session.delete(server)
|
|
101
|
+
await session.commit()
|
|
102
|
+
logger.debug("Deleted MCP server: %s", server.name)
|
|
103
|
+
return True
|
|
104
|
+
logger.warning("MCP server with ID %s not found for deletion", server_id)
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SystemPromptRepository:
|
|
109
|
+
"""Repository class for system prompt database operations.
|
|
110
|
+
|
|
111
|
+
Implements append-only versioning with full CRUD capabilities.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
async def get_all() -> list[SystemPrompt]:
|
|
116
|
+
"""Retrieve all system prompt versions ordered by version descending."""
|
|
117
|
+
async with rx.asession() as session:
|
|
118
|
+
result = await session.exec(
|
|
119
|
+
SystemPrompt.select().order_by(SystemPrompt.version.desc())
|
|
120
|
+
)
|
|
121
|
+
return result.all()
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
async def get_latest() -> SystemPrompt | None:
|
|
125
|
+
"""Retrieve the latest system prompt version."""
|
|
126
|
+
async with rx.asession() as session:
|
|
127
|
+
result = await session.exec(
|
|
128
|
+
SystemPrompt.select().order_by(SystemPrompt.version.desc()).limit(1)
|
|
129
|
+
)
|
|
130
|
+
return result.first()
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
async def get_by_id(prompt_id: int) -> SystemPrompt | None:
|
|
134
|
+
"""Retrieve a system prompt by ID."""
|
|
135
|
+
async with rx.asession() as session:
|
|
136
|
+
result = await session.exec(
|
|
137
|
+
SystemPrompt.select().where(SystemPrompt.id == prompt_id)
|
|
138
|
+
)
|
|
139
|
+
return result.first()
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
async def create(prompt: str, user_id: int) -> SystemPrompt:
|
|
143
|
+
"""Neue System Prompt Version anlegen.
|
|
144
|
+
|
|
145
|
+
Version ist fortlaufende Ganzzahl, beginnend bei 1.
|
|
146
|
+
"""
|
|
147
|
+
async with rx.asession() as session:
|
|
148
|
+
result = await session.exec(
|
|
149
|
+
SystemPrompt.select().order_by(SystemPrompt.version.desc()).limit(1)
|
|
150
|
+
)
|
|
151
|
+
latest = result.first()
|
|
152
|
+
next_version = (latest.version + 1) if latest else 1
|
|
153
|
+
|
|
154
|
+
name = f"Version {next_version}"
|
|
155
|
+
|
|
156
|
+
system_prompt = SystemPrompt(
|
|
157
|
+
name=name,
|
|
158
|
+
prompt=prompt,
|
|
159
|
+
version=next_version,
|
|
160
|
+
user_id=user_id,
|
|
161
|
+
created_at=datetime.now(UTC),
|
|
162
|
+
)
|
|
163
|
+
session.add(system_prompt)
|
|
164
|
+
await session.commit()
|
|
165
|
+
await session.refresh(system_prompt)
|
|
166
|
+
|
|
167
|
+
logger.info(
|
|
168
|
+
"Created system prompt version %s for user %s",
|
|
169
|
+
next_version,
|
|
170
|
+
user_id,
|
|
171
|
+
)
|
|
172
|
+
return system_prompt
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
async def delete(prompt_id: int) -> bool:
|
|
176
|
+
"""Delete a system prompt version by ID."""
|
|
177
|
+
async with rx.asession() as session:
|
|
178
|
+
result = await session.exec(
|
|
179
|
+
SystemPrompt.select().where(SystemPrompt.id == prompt_id)
|
|
180
|
+
)
|
|
181
|
+
prompt = result.first()
|
|
182
|
+
if prompt:
|
|
183
|
+
await session.delete(prompt)
|
|
184
|
+
await session.commit()
|
|
185
|
+
logger.info("Deleted system prompt version: %s", prompt.version)
|
|
186
|
+
return True
|
|
187
|
+
logger.warning(
|
|
188
|
+
"System prompt with ID %s not found for deletion",
|
|
189
|
+
prompt_id,
|
|
190
|
+
)
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ThreadRepository:
|
|
195
|
+
"""Repository class for Thread database operations."""
|
|
196
|
+
|
|
197
|
+
@staticmethod
|
|
198
|
+
async def get_by_user(user_id: int) -> list[ThreadModel]:
|
|
199
|
+
"""Retrieve all threads for a user."""
|
|
200
|
+
async with rx.asession() as session:
|
|
201
|
+
result = await session.exec(
|
|
202
|
+
AssistantThread.select()
|
|
203
|
+
.where(AssistantThread.user_id == user_id)
|
|
204
|
+
.order_by(AssistantThread.updated_at.desc())
|
|
205
|
+
)
|
|
206
|
+
threads = result.all()
|
|
207
|
+
return [
|
|
208
|
+
ThreadModel(
|
|
209
|
+
thread_id=t.thread_id,
|
|
210
|
+
title=t.title,
|
|
211
|
+
state=ThreadStatus(t.state),
|
|
212
|
+
ai_model=t.ai_model,
|
|
213
|
+
active=t.active,
|
|
214
|
+
messages=[Message(**m) for m in t.messages],
|
|
215
|
+
)
|
|
216
|
+
for t in threads
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
async def save_thread(thread: ThreadModel, user_id: int) -> None:
|
|
221
|
+
"""Save or update a thread."""
|
|
222
|
+
async with rx.asession() as session:
|
|
223
|
+
result = await session.exec(
|
|
224
|
+
AssistantThread.select().where(
|
|
225
|
+
AssistantThread.thread_id == thread.thread_id
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
db_thread = result.first()
|
|
229
|
+
|
|
230
|
+
messages_dict = [m.dict() for m in thread.messages]
|
|
231
|
+
|
|
232
|
+
if db_thread:
|
|
233
|
+
# Ensure user owns the thread or handle shared threads logic if needed
|
|
234
|
+
# For now, we assume thread_id is unique enough,
|
|
235
|
+
# but checking user_id is safer
|
|
236
|
+
if db_thread.user_id != user_id:
|
|
237
|
+
logger.warning(
|
|
238
|
+
"User %s tried to update thread %s belonging to user %s",
|
|
239
|
+
user_id,
|
|
240
|
+
thread.thread_id,
|
|
241
|
+
db_thread.user_id,
|
|
242
|
+
)
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
db_thread.title = thread.title
|
|
246
|
+
db_thread.state = thread.state.value
|
|
247
|
+
db_thread.ai_model = thread.ai_model
|
|
248
|
+
db_thread.active = thread.active
|
|
249
|
+
db_thread.messages = messages_dict
|
|
250
|
+
session.add(db_thread)
|
|
251
|
+
else:
|
|
252
|
+
db_thread = AssistantThread(
|
|
253
|
+
thread_id=thread.thread_id,
|
|
254
|
+
user_id=user_id,
|
|
255
|
+
title=thread.title,
|
|
256
|
+
state=thread.state.value,
|
|
257
|
+
ai_model=thread.ai_model,
|
|
258
|
+
active=thread.active,
|
|
259
|
+
messages=messages_dict,
|
|
260
|
+
)
|
|
261
|
+
session.add(db_thread)
|
|
262
|
+
|
|
263
|
+
await session.commit()
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
async def delete_thread(thread_id: str, user_id: int) -> None:
|
|
267
|
+
"""Delete a thread."""
|
|
268
|
+
async with rx.asession() as session:
|
|
269
|
+
result = await session.exec(
|
|
270
|
+
AssistantThread.select().where(
|
|
271
|
+
AssistantThread.thread_id == thread_id,
|
|
272
|
+
AssistantThread.user_id == user_id,
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
thread = result.first()
|
|
276
|
+
if thread:
|
|
277
|
+
await session.delete(thread)
|
|
278
|
+
await session.commit()
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
async def get_summaries_by_user(user_id: int) -> list[ThreadModel]:
|
|
282
|
+
"""Retrieve thread summaries (no messages) for a user."""
|
|
283
|
+
async with rx.asession() as session:
|
|
284
|
+
result = await session.exec(
|
|
285
|
+
AssistantThread.select()
|
|
286
|
+
.where(AssistantThread.user_id == user_id)
|
|
287
|
+
.options(defer(AssistantThread.messages))
|
|
288
|
+
.order_by(AssistantThread.updated_at.desc())
|
|
289
|
+
)
|
|
290
|
+
threads = result.all()
|
|
291
|
+
return [
|
|
292
|
+
ThreadModel(
|
|
293
|
+
thread_id=t.thread_id,
|
|
294
|
+
title=t.title,
|
|
295
|
+
state=ThreadStatus(t.state),
|
|
296
|
+
ai_model=t.ai_model,
|
|
297
|
+
active=t.active,
|
|
298
|
+
messages=[], # Empty messages for summary
|
|
299
|
+
)
|
|
300
|
+
for t in threads
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
async def get_thread_by_id(thread_id: str, user_id: int) -> ThreadModel | None:
|
|
305
|
+
"""Retrieve a full thread by ID."""
|
|
306
|
+
async with rx.asession() as session:
|
|
307
|
+
result = await session.exec(
|
|
308
|
+
AssistantThread.select().where(
|
|
309
|
+
AssistantThread.thread_id == thread_id,
|
|
310
|
+
AssistantThread.user_id == user_id,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
t = result.first()
|
|
314
|
+
if not t:
|
|
315
|
+
return None
|
|
316
|
+
return ThreadModel(
|
|
317
|
+
thread_id=t.thread_id,
|
|
318
|
+
title=t.title,
|
|
319
|
+
state=ThreadStatus(t.state),
|
|
320
|
+
ai_model=t.ai_model,
|
|
321
|
+
active=t.active,
|
|
322
|
+
messages=[Message(**m) for m in t.messages],
|
|
323
|
+
)
|