appkit-assistant 0.16.3__tar.gz → 0.17.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 (42) hide show
  1. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/PKG-INFO +4 -1
  2. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/pyproject.toml +4 -1
  3. appkit_assistant-0.17.1/src/appkit_assistant/backend/file_manager.py +117 -0
  4. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/models.py +12 -0
  5. appkit_assistant-0.17.1/src/appkit_assistant/backend/processors/claude_base.py +178 -0
  6. appkit_assistant-0.17.1/src/appkit_assistant/backend/processors/claude_responses_processor.py +923 -0
  7. appkit_assistant-0.17.1/src/appkit_assistant/backend/processors/gemini_base.py +84 -0
  8. appkit_assistant-0.17.1/src/appkit_assistant/backend/processors/gemini_responses_processor.py +726 -0
  9. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/processors/lorem_ipsum_processor.py +2 -0
  10. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/processors/openai_base.py +10 -10
  11. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/processors/openai_chat_completion_processor.py +25 -8
  12. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/processors/openai_responses_processor.py +22 -15
  13. {appkit_assistant-0.16.3/src/appkit_assistant/logic → appkit_assistant-0.17.1/src/appkit_assistant/backend}/response_accumulator.py +58 -11
  14. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/__init__.py +2 -0
  15. appkit_assistant-0.17.1/src/appkit_assistant/components/composer.py +241 -0
  16. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/message.py +218 -50
  17. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/thread.py +2 -1
  18. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/configuration.py +2 -0
  19. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/state/thread_state.py +239 -5
  20. appkit_assistant-0.16.3/src/appkit_assistant/components/composer.py +0 -154
  21. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/.gitignore +0 -0
  22. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/README.md +0 -0
  23. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/docs/assistant.png +0 -0
  24. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/mcp_auth_service.py +0 -0
  25. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/model_manager.py +0 -0
  26. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/processor.py +0 -0
  27. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/processors/perplexity_processor.py +0 -0
  28. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/repositories.py +0 -0
  29. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/services/thread_service.py +0 -0
  30. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/backend/system_prompt_cache.py +0 -0
  31. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/composer_key_handler.py +0 -0
  32. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/mcp_oauth.py +0 -0
  33. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/mcp_server_dialogs.py +0 -0
  34. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/mcp_server_table.py +0 -0
  35. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/system_prompt_editor.py +0 -0
  36. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/threadlist.py +0 -0
  37. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/components/tools_modal.py +0 -0
  38. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/pages.py +0 -0
  39. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/state/mcp_oauth_state.py +0 -0
  40. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/state/mcp_server_state.py +0 -0
  41. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/src/appkit_assistant/state/system_prompt_state.py +0 -0
  42. {appkit_assistant-0.16.3 → appkit_assistant-0.17.1}/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.16.3
3
+ Version: 0.17.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
@@ -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.16.3"
13
+ version = "0.17.1"
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
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import uuid
2
3
  from datetime import UTC, datetime
3
4
  from enum import StrEnum
4
5
  from typing import Any
@@ -82,10 +83,13 @@ class MessageType(StrEnum):
82
83
 
83
84
 
84
85
  class Message(BaseModel):
86
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()))
85
87
  text: str
88
+ original_text: str | None = None # To store original text if edited
86
89
  editable: bool = False
87
90
  type: MessageType
88
91
  done: bool = False
92
+ attachments: list[str] = [] # List of filenames for display
89
93
 
90
94
 
91
95
  class ThinkingType(StrEnum):
@@ -131,6 +135,14 @@ class Suggestion(BaseModel):
131
135
  icon: str = ""
132
136
 
133
137
 
138
+ class UploadedFile(BaseModel):
139
+ """Model for tracking uploaded files in the composer."""
140
+
141
+ filename: str
142
+ file_path: str
143
+ size: int = 0
144
+
145
+
134
146
  class ThreadModel(BaseModel):
135
147
  thread_id: str
136
148
  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, ""