appkit-assistant 0.16.2__tar.gz → 0.17.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.16.2 → appkit_assistant-0.17.0}/PKG-INFO +4 -1
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/pyproject.toml +4 -1
- appkit_assistant-0.17.0/src/appkit_assistant/backend/file_manager.py +117 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/models.py +9 -0
- appkit_assistant-0.17.0/src/appkit_assistant/backend/processors/claude_base.py +178 -0
- appkit_assistant-0.17.0/src/appkit_assistant/backend/processors/claude_responses_processor.py +923 -0
- appkit_assistant-0.17.0/src/appkit_assistant/backend/processors/gemini_base.py +84 -0
- appkit_assistant-0.17.0/src/appkit_assistant/backend/processors/gemini_responses_processor.py +723 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +2 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/processors/openai_base.py +10 -10
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/processors/openai_responses_processor.py +22 -15
- {appkit_assistant-0.16.2/src/appkit_assistant/logic → appkit_assistant-0.17.0/src/appkit_assistant/backend}/response_accumulator.py +50 -11
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/__init__.py +2 -0
- appkit_assistant-0.17.0/src/appkit_assistant/components/composer.py +241 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/message.py +50 -17
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/thread.py +2 -1
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/configuration.py +2 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/state/thread_state.py +103 -5
- appkit_assistant-0.16.2/src/appkit_assistant/components/composer.py +0 -154
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/.gitignore +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/README.md +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/docs/assistant.png +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/mcp_auth_service.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/model_manager.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/processor.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/repositories.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/services/thread_service.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/backend/system_prompt_cache.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/composer_key_handler.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/mcp_oauth.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/mcp_server_dialogs.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/mcp_server_table.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/system_prompt_editor.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/threadlist.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/components/tools_modal.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/pages.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/state/mcp_oauth_state.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/state/mcp_server_state.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/state/system_prompt_state.py +0 -0
- {appkit_assistant-0.16.2 → appkit_assistant-0.17.0}/src/appkit_assistant/state/thread_list_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.17.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
|
|
@@ -16,9 +16,12 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
17
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
18
18
|
Requires-Python: >=3.13
|
|
19
|
+
Requires-Dist: anthropic>=0.40.0
|
|
19
20
|
Requires-Dist: appkit-commons
|
|
20
21
|
Requires-Dist: appkit-mantine
|
|
21
22
|
Requires-Dist: appkit-ui
|
|
23
|
+
Requires-Dist: google-genai>=1.52.0
|
|
24
|
+
Requires-Dist: mcp>=1.0.0
|
|
22
25
|
Requires-Dist: openai>=2.3.0
|
|
23
26
|
Requires-Dist: reflex>=0.8.22
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
dependencies = [
|
|
3
|
+
"anthropic>=0.40.0",
|
|
3
4
|
"appkit-commons",
|
|
4
5
|
"appkit-mantine",
|
|
5
6
|
"appkit-ui",
|
|
7
|
+
"google-genai>=1.52.0",
|
|
8
|
+
"mcp>=1.0.0",
|
|
6
9
|
"openai>=2.3.0",
|
|
7
10
|
"reflex>=0.8.22",
|
|
8
11
|
]
|
|
9
12
|
name = "appkit-assistant"
|
|
10
|
-
version = "0.
|
|
13
|
+
version = "0.17.0"
|
|
11
14
|
description = "Add your description here"
|
|
12
15
|
readme = "README.md"
|
|
13
16
|
authors = [{ name = "Jens Rehpöhler" }]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""File management utilities for assistant uploads.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for managing uploaded files in user-specific
|
|
4
|
+
directories. Files are stored temporarily and cleaned up after processing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Base directory for uploaded files (relative to project root)
|
|
14
|
+
UPLOAD_BASE_DIR = Path("uploaded_files")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_user_upload_directory(user_id: str) -> Path:
|
|
18
|
+
"""Get the upload directory for a specific user.
|
|
19
|
+
|
|
20
|
+
Creates the directory if it doesn't exist.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
user_id: The user's unique identifier.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Path to the user's upload directory.
|
|
27
|
+
"""
|
|
28
|
+
user_dir = UPLOAD_BASE_DIR / user_id
|
|
29
|
+
user_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
logger.debug("User upload dir: %s", user_dir)
|
|
31
|
+
return user_dir
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _make_unique_filename(target_path: Path) -> Path:
|
|
35
|
+
"""Generate a unique filename if target already exists.
|
|
36
|
+
|
|
37
|
+
Appends _1, _2, etc. before the extension until a unique name is found.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
target_path: The desired file path.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A unique file path that doesn't exist yet.
|
|
44
|
+
"""
|
|
45
|
+
if not target_path.exists():
|
|
46
|
+
return target_path
|
|
47
|
+
|
|
48
|
+
stem = target_path.stem
|
|
49
|
+
suffix = target_path.suffix
|
|
50
|
+
parent = target_path.parent
|
|
51
|
+
counter = 1
|
|
52
|
+
|
|
53
|
+
while True:
|
|
54
|
+
new_name = f"{stem}_{counter}{suffix}"
|
|
55
|
+
new_path = parent / new_name
|
|
56
|
+
if not new_path.exists():
|
|
57
|
+
return new_path
|
|
58
|
+
counter += 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def move_to_user_directory(temp_path: str, user_id: str) -> str:
|
|
62
|
+
"""Move a file from temporary location to user's upload directory.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
temp_path: Path to the temporary file (from browser upload).
|
|
66
|
+
user_id: The user's unique identifier.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Absolute path to the file in the user's directory.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
FileNotFoundError: If the source file doesn't exist.
|
|
73
|
+
"""
|
|
74
|
+
source = Path(temp_path)
|
|
75
|
+
if not source.exists():
|
|
76
|
+
raise FileNotFoundError(f"Source file not found: {temp_path}")
|
|
77
|
+
|
|
78
|
+
user_dir = get_user_upload_directory(user_id)
|
|
79
|
+
target = user_dir / source.name
|
|
80
|
+
target = _make_unique_filename(target)
|
|
81
|
+
|
|
82
|
+
shutil.move(str(source), str(target))
|
|
83
|
+
logger.info("Moved file to user directory: %s → %s", temp_path, target)
|
|
84
|
+
|
|
85
|
+
return str(target.absolute())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cleanup_uploaded_files(file_paths: list[str]) -> None:
|
|
89
|
+
"""Delete uploaded files from disk.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
file_paths: List of absolute file paths to delete.
|
|
93
|
+
"""
|
|
94
|
+
deleted_count = 0
|
|
95
|
+
for file_path in file_paths:
|
|
96
|
+
try:
|
|
97
|
+
Path(file_path).unlink(missing_ok=True)
|
|
98
|
+
deleted_count += 1
|
|
99
|
+
except OSError as e:
|
|
100
|
+
logger.warning("Failed to delete file %s: %s", file_path, e)
|
|
101
|
+
|
|
102
|
+
logger.debug("Cleaned up %d uploaded files", deleted_count)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_file_size(file_path: str) -> int:
|
|
106
|
+
"""Get the size of a file in bytes.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
file_path: Path to the file.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
File size in bytes, or 0 if file doesn't exist.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
return Path(file_path).stat().st_size
|
|
116
|
+
except OSError:
|
|
117
|
+
return 0
|
|
@@ -86,6 +86,7 @@ class Message(BaseModel):
|
|
|
86
86
|
editable: bool = False
|
|
87
87
|
type: MessageType
|
|
88
88
|
done: bool = False
|
|
89
|
+
attachments: list[str] = [] # List of filenames for display
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
class ThinkingType(StrEnum):
|
|
@@ -131,6 +132,14 @@ class Suggestion(BaseModel):
|
|
|
131
132
|
icon: str = ""
|
|
132
133
|
|
|
133
134
|
|
|
135
|
+
class UploadedFile(BaseModel):
|
|
136
|
+
"""Model for tracking uploaded files in the composer."""
|
|
137
|
+
|
|
138
|
+
filename: str
|
|
139
|
+
file_path: str
|
|
140
|
+
size: int = 0
|
|
141
|
+
|
|
142
|
+
|
|
134
143
|
class ThreadModel(BaseModel):
|
|
135
144
|
thread_id: str
|
|
136
145
|
title: str = ""
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude base processor for generating AI responses using Anthropic's Claude API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Final
|
|
10
|
+
|
|
11
|
+
from anthropic import AsyncAnthropic
|
|
12
|
+
|
|
13
|
+
from appkit_assistant.backend.models import (
|
|
14
|
+
AIModel,
|
|
15
|
+
Chunk,
|
|
16
|
+
MCPServer,
|
|
17
|
+
Message,
|
|
18
|
+
)
|
|
19
|
+
from appkit_assistant.backend.processor import Processor
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
CLAUDE_HAIKU_4_5: Final = AIModel(
|
|
24
|
+
id="claude-haiku-4.5",
|
|
25
|
+
text="Claude 4.5 Haiku",
|
|
26
|
+
icon="anthropic",
|
|
27
|
+
model="claude-haiku-4-5",
|
|
28
|
+
stream=True,
|
|
29
|
+
supports_attachments=False,
|
|
30
|
+
supports_tools=True,
|
|
31
|
+
temperature=1.0,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
CLAUDE_SONNET_4_5: Final = AIModel(
|
|
35
|
+
id="claude-sonnet-4.5",
|
|
36
|
+
text="Claude 4.5 Sonnet",
|
|
37
|
+
icon="anthropic",
|
|
38
|
+
model="claude-sonnet-4-5",
|
|
39
|
+
stream=True,
|
|
40
|
+
supports_attachments=False,
|
|
41
|
+
supports_tools=True,
|
|
42
|
+
temperature=1.0,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BaseClaudeProcessor(Processor, ABC):
|
|
47
|
+
"""Base class for Claude processors with common initialization and utilities."""
|
|
48
|
+
|
|
49
|
+
# Extended thinking budget (fixed at 10k tokens)
|
|
50
|
+
THINKING_BUDGET_TOKENS: Final[int] = 10000
|
|
51
|
+
|
|
52
|
+
# Max file size (5MB)
|
|
53
|
+
MAX_FILE_SIZE: Final[int] = 5 * 1024 * 1024
|
|
54
|
+
|
|
55
|
+
# Allowed file extensions
|
|
56
|
+
ALLOWED_EXTENSIONS: Final[set[str]] = {
|
|
57
|
+
"pdf",
|
|
58
|
+
"png",
|
|
59
|
+
"jpg",
|
|
60
|
+
"jpeg",
|
|
61
|
+
"xlsx",
|
|
62
|
+
"csv",
|
|
63
|
+
"docx",
|
|
64
|
+
"pptx",
|
|
65
|
+
"md",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Image extensions (for determining content type)
|
|
69
|
+
IMAGE_EXTENSIONS: Final[set[str]] = {"png", "jpg", "jpeg", "gif", "webp"}
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
models: dict[str, AIModel],
|
|
74
|
+
api_key: str | None = None,
|
|
75
|
+
base_url: str | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Initialize the base Claude processor.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
models: Dictionary of supported AI models
|
|
81
|
+
api_key: API key for Anthropic Claude API (or Azure API key)
|
|
82
|
+
base_url: Base URL for Azure-hosted Claude (optional)
|
|
83
|
+
"""
|
|
84
|
+
self.api_key = api_key
|
|
85
|
+
self.base_url = base_url
|
|
86
|
+
self.models = models
|
|
87
|
+
self.client: AsyncAnthropic | None = None
|
|
88
|
+
|
|
89
|
+
if self.api_key:
|
|
90
|
+
if self.base_url:
|
|
91
|
+
# Azure-hosted Claude
|
|
92
|
+
self.client = AsyncAnthropic(
|
|
93
|
+
api_key=self.api_key,
|
|
94
|
+
base_url=self.base_url,
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
# Direct Anthropic API
|
|
98
|
+
self.client = AsyncAnthropic(api_key=self.api_key)
|
|
99
|
+
else:
|
|
100
|
+
logger.warning("No Claude API key found. Processor will not work.")
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
async def process(
|
|
104
|
+
self,
|
|
105
|
+
messages: list[Message],
|
|
106
|
+
model_id: str,
|
|
107
|
+
files: list[str] | None = None,
|
|
108
|
+
mcp_servers: list[MCPServer] | None = None,
|
|
109
|
+
payload: dict[str, Any] | None = None,
|
|
110
|
+
user_id: int | None = None,
|
|
111
|
+
) -> AsyncGenerator[Chunk, None]:
|
|
112
|
+
"""Process messages and generate AI response chunks."""
|
|
113
|
+
|
|
114
|
+
def get_supported_models(self) -> dict[str, AIModel]:
|
|
115
|
+
"""Return supported models if API key is available."""
|
|
116
|
+
return self.models if self.api_key else {}
|
|
117
|
+
|
|
118
|
+
def _get_file_extension(self, file_path: str) -> str:
|
|
119
|
+
"""Extract file extension from path."""
|
|
120
|
+
return file_path.rsplit(".", 1)[-1].lower() if "." in file_path else ""
|
|
121
|
+
|
|
122
|
+
def _is_image_file(self, file_path: str) -> bool:
|
|
123
|
+
"""Check if file is an image based on extension."""
|
|
124
|
+
ext = self._get_file_extension(file_path)
|
|
125
|
+
return ext in self.IMAGE_EXTENSIONS
|
|
126
|
+
|
|
127
|
+
def _get_media_type(self, file_path: str) -> str:
|
|
128
|
+
"""Get MIME type for a file based on extension."""
|
|
129
|
+
ext = self._get_file_extension(file_path)
|
|
130
|
+
media_types = {
|
|
131
|
+
"pdf": "application/pdf",
|
|
132
|
+
"png": "image/png",
|
|
133
|
+
"jpg": "image/jpeg",
|
|
134
|
+
"jpeg": "image/jpeg",
|
|
135
|
+
"gif": "image/gif",
|
|
136
|
+
"webp": "image/webp",
|
|
137
|
+
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
138
|
+
"csv": "text/csv",
|
|
139
|
+
"docx": (
|
|
140
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml"
|
|
141
|
+
".document"
|
|
142
|
+
),
|
|
143
|
+
"pptx": (
|
|
144
|
+
"application/vnd.openxmlformats-officedocument.presentationml"
|
|
145
|
+
".presentation"
|
|
146
|
+
),
|
|
147
|
+
"md": "text/markdown",
|
|
148
|
+
"txt": "text/plain",
|
|
149
|
+
}
|
|
150
|
+
return media_types.get(ext, "application/octet-stream")
|
|
151
|
+
|
|
152
|
+
def _validate_file(self, file_path: str) -> tuple[bool, str]:
|
|
153
|
+
"""Validate file for upload.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
file_path: Path to the file
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Tuple of (is_valid, error_message)
|
|
160
|
+
"""
|
|
161
|
+
path = Path(file_path)
|
|
162
|
+
|
|
163
|
+
# Check if file exists
|
|
164
|
+
if not path.exists():
|
|
165
|
+
return False, f"File not found: {file_path}"
|
|
166
|
+
|
|
167
|
+
# Check extension
|
|
168
|
+
ext = self._get_file_extension(file_path)
|
|
169
|
+
if ext not in self.ALLOWED_EXTENSIONS:
|
|
170
|
+
return False, f"Unsupported file type: {ext}"
|
|
171
|
+
|
|
172
|
+
# Check file size
|
|
173
|
+
file_size = path.stat().st_size
|
|
174
|
+
if file_size > self.MAX_FILE_SIZE:
|
|
175
|
+
size_mb = file_size / (1024 * 1024)
|
|
176
|
+
return False, f"File too large: {size_mb:.1f}MB (max 5MB)"
|
|
177
|
+
|
|
178
|
+
return True, ""
|