appkit-assistant 0.17.3__py3-none-any.whl → 1.0.1__py3-none-any.whl
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/backend/{models.py → database/models.py} +32 -132
- appkit_assistant/backend/{repositories.py → database/repositories.py} +93 -1
- appkit_assistant/backend/model_manager.py +5 -5
- appkit_assistant/backend/models/__init__.py +28 -0
- appkit_assistant/backend/models/anthropic.py +31 -0
- appkit_assistant/backend/models/google.py +27 -0
- appkit_assistant/backend/models/openai.py +50 -0
- appkit_assistant/backend/models/perplexity.py +56 -0
- appkit_assistant/backend/processors/__init__.py +29 -0
- appkit_assistant/backend/processors/claude_responses_processor.py +205 -387
- appkit_assistant/backend/processors/gemini_responses_processor.py +290 -352
- appkit_assistant/backend/processors/lorem_ipsum_processor.py +6 -4
- appkit_assistant/backend/processors/mcp_mixin.py +297 -0
- appkit_assistant/backend/processors/openai_base.py +11 -125
- appkit_assistant/backend/processors/openai_chat_completion_processor.py +5 -3
- appkit_assistant/backend/processors/openai_responses_processor.py +480 -402
- appkit_assistant/backend/processors/perplexity_processor.py +156 -79
- appkit_assistant/backend/{processor.py → processors/processor_base.py} +7 -2
- appkit_assistant/backend/processors/streaming_base.py +188 -0
- appkit_assistant/backend/schemas.py +138 -0
- appkit_assistant/backend/services/auth_error_detector.py +99 -0
- appkit_assistant/backend/services/chunk_factory.py +273 -0
- appkit_assistant/backend/services/citation_handler.py +292 -0
- appkit_assistant/backend/services/file_cleanup_service.py +316 -0
- appkit_assistant/backend/services/file_upload_service.py +903 -0
- appkit_assistant/backend/services/file_validation.py +138 -0
- appkit_assistant/backend/{mcp_auth_service.py → services/mcp_auth_service.py} +4 -2
- appkit_assistant/backend/services/mcp_token_service.py +61 -0
- appkit_assistant/backend/services/message_converter.py +289 -0
- appkit_assistant/backend/services/openai_client_service.py +120 -0
- appkit_assistant/backend/{response_accumulator.py → services/response_accumulator.py} +163 -1
- appkit_assistant/backend/services/system_prompt_builder.py +89 -0
- appkit_assistant/backend/services/thread_service.py +5 -3
- appkit_assistant/backend/system_prompt_cache.py +3 -3
- appkit_assistant/components/__init__.py +8 -4
- appkit_assistant/components/composer.py +59 -24
- appkit_assistant/components/file_manager.py +623 -0
- appkit_assistant/components/mcp_server_dialogs.py +12 -20
- appkit_assistant/components/mcp_server_table.py +12 -2
- appkit_assistant/components/message.py +119 -2
- appkit_assistant/components/thread.py +1 -1
- appkit_assistant/components/threadlist.py +4 -2
- appkit_assistant/components/tools_modal.py +37 -20
- appkit_assistant/configuration.py +12 -0
- appkit_assistant/state/file_manager_state.py +697 -0
- appkit_assistant/state/mcp_oauth_state.py +3 -3
- appkit_assistant/state/mcp_server_state.py +47 -2
- appkit_assistant/state/system_prompt_state.py +1 -1
- appkit_assistant/state/thread_list_state.py +99 -5
- appkit_assistant/state/thread_state.py +88 -9
- {appkit_assistant-0.17.3.dist-info → appkit_assistant-1.0.1.dist-info}/METADATA +8 -6
- appkit_assistant-1.0.1.dist-info/RECORD +58 -0
- appkit_assistant/backend/processors/claude_base.py +0 -178
- appkit_assistant/backend/processors/gemini_base.py +0 -84
- appkit_assistant-0.17.3.dist-info/RECORD +0 -39
- /appkit_assistant/backend/{file_manager.py → services/file_manager.py} +0 -0
- {appkit_assistant-0.17.3.dist-info → appkit_assistant-1.0.1.dist-info}/WHEEL +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
3
|
+
import re
|
|
2
4
|
import uuid
|
|
3
5
|
from typing import Any
|
|
4
6
|
|
|
5
|
-
from appkit_assistant.backend.
|
|
7
|
+
from appkit_assistant.backend.schemas import (
|
|
6
8
|
Chunk,
|
|
7
9
|
ChunkType,
|
|
8
10
|
Message,
|
|
@@ -14,6 +16,49 @@ from appkit_assistant.backend.models import (
|
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
19
|
+
# Minimum number of consecutive links required to format as a list
|
|
20
|
+
MIN_LINKS_FOR_LIST_FORMAT = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _format_consecutive_links_as_list(text: str) -> str:
|
|
24
|
+
"""Format multiple consecutive markdown links as a bullet list.
|
|
25
|
+
|
|
26
|
+
Detects patterns where markdown links are concatenated directly without
|
|
27
|
+
spacing (e.g., `[text1](url1)[text2](url2)`) and converts them to a
|
|
28
|
+
properly formatted bullet list with human-readable link texts.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
text: The markdown text to process.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The text with consecutive links formatted as a bullet list.
|
|
35
|
+
"""
|
|
36
|
+
# Pattern to match consecutive markdown links: [text](url)[text](url)
|
|
37
|
+
# This regex matches two or more consecutive links
|
|
38
|
+
consecutive_links_pattern = re.compile(
|
|
39
|
+
r"(\[[^\]]+\]\([^)]+\))(\[[^\]]+\]\([^)]+\))+", re.MULTILINE
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def format_links_match(match: re.Match[str]) -> str:
|
|
43
|
+
"""Convert matched consecutive links to a bullet list."""
|
|
44
|
+
full_match = match.group(0)
|
|
45
|
+
|
|
46
|
+
# Extract all individual links from the match
|
|
47
|
+
link_pattern = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
|
|
48
|
+
links = link_pattern.findall(full_match)
|
|
49
|
+
|
|
50
|
+
if len(links) < MIN_LINKS_FOR_LIST_FORMAT:
|
|
51
|
+
return full_match
|
|
52
|
+
|
|
53
|
+
# Format as a bullet list with proper spacing
|
|
54
|
+
formatted_links = "\n\n**Quellen:**\n"
|
|
55
|
+
for link_text, url in links:
|
|
56
|
+
formatted_links += f"- [{link_text}]({url})\n"
|
|
57
|
+
|
|
58
|
+
return formatted_links
|
|
59
|
+
|
|
60
|
+
return consecutive_links_pattern.sub(format_links_match, text)
|
|
61
|
+
|
|
17
62
|
|
|
18
63
|
class ResponseAccumulator:
|
|
19
64
|
"""
|
|
@@ -63,6 +108,8 @@ class ResponseAccumulator:
|
|
|
63
108
|
if chunk.type == ChunkType.TEXT:
|
|
64
109
|
if self.messages and self.messages[-1].type == MessageType.ASSISTANT:
|
|
65
110
|
self.messages[-1].text += chunk.text
|
|
111
|
+
# Extract citations from metadata and add as annotations
|
|
112
|
+
self._extract_citations_to_annotations(chunk)
|
|
66
113
|
|
|
67
114
|
elif chunk.type in (ChunkType.THINKING, ChunkType.THINKING_RESULT):
|
|
68
115
|
self._handle_reasoning_chunk(chunk)
|
|
@@ -79,10 +126,18 @@ class ResponseAccumulator:
|
|
|
79
126
|
|
|
80
127
|
elif chunk.type == ChunkType.COMPLETION:
|
|
81
128
|
self.show_thinking = False
|
|
129
|
+
# Post-process message text to format consecutive links as list
|
|
130
|
+
self._format_message_links()
|
|
82
131
|
|
|
83
132
|
elif chunk.type == ChunkType.AUTH_REQUIRED:
|
|
84
133
|
self._handle_auth_required_chunk(chunk)
|
|
85
134
|
|
|
135
|
+
elif chunk.type == ChunkType.PROCESSING:
|
|
136
|
+
self._handle_processing_chunk(chunk)
|
|
137
|
+
|
|
138
|
+
elif chunk.type == ChunkType.ANNOTATION:
|
|
139
|
+
self._handle_annotation_chunk(chunk)
|
|
140
|
+
|
|
86
141
|
elif chunk.type == ChunkType.ERROR:
|
|
87
142
|
# We append it to the message text if it's not a hard error,
|
|
88
143
|
# or creates a new message?
|
|
@@ -254,6 +309,13 @@ class ResponseAccumulator:
|
|
|
254
309
|
item.status = status
|
|
255
310
|
item.result = result
|
|
256
311
|
item.error = error
|
|
312
|
+
# Also update tool_name from result if item is missing it
|
|
313
|
+
if (
|
|
314
|
+
display_name
|
|
315
|
+
and display_name != "Unknown"
|
|
316
|
+
and (not item.tool_name or item.tool_name == "Unknown")
|
|
317
|
+
):
|
|
318
|
+
item.tool_name = display_name
|
|
257
319
|
elif chunk.type == ChunkType.ACTION:
|
|
258
320
|
item.text += f"\n---\nAktion: {chunk.text}"
|
|
259
321
|
|
|
@@ -273,3 +335,103 @@ class ResponseAccumulator:
|
|
|
273
335
|
self.pending_auth_server_name = chunk.chunk_metadata.get("server_name", "")
|
|
274
336
|
self.pending_auth_url = chunk.chunk_metadata.get("auth_url", "")
|
|
275
337
|
self.auth_required = True
|
|
338
|
+
|
|
339
|
+
def _handle_processing_chunk(self, chunk: Chunk) -> None:
|
|
340
|
+
"""Handle file processing progress chunks."""
|
|
341
|
+
status = chunk.chunk_metadata.get("status", "")
|
|
342
|
+
|
|
343
|
+
# Skip empty/skipped chunks (used for signaling completion without UI)
|
|
344
|
+
if not chunk.text or status == "skipped":
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
# Show thinking panel when processing
|
|
348
|
+
self.show_thinking = True
|
|
349
|
+
self.current_activity = chunk.text
|
|
350
|
+
|
|
351
|
+
# Determine item status based on metadata
|
|
352
|
+
if status == "completed":
|
|
353
|
+
item_status = ThinkingStatus.COMPLETED
|
|
354
|
+
elif status in ("failed", "timeout"):
|
|
355
|
+
item_status = ThinkingStatus.ERROR
|
|
356
|
+
else:
|
|
357
|
+
item_status = ThinkingStatus.IN_PROGRESS
|
|
358
|
+
|
|
359
|
+
# Use a single processing item that gets updated
|
|
360
|
+
item = self._get_or_create_thinking_item(
|
|
361
|
+
"file_processing",
|
|
362
|
+
ThinkingType.PROCESSING,
|
|
363
|
+
text=chunk.text,
|
|
364
|
+
status=item_status,
|
|
365
|
+
tool_name="Dateiverarbeitung",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
item.text = chunk.text
|
|
369
|
+
item.status = item_status
|
|
370
|
+
|
|
371
|
+
# Store error if present
|
|
372
|
+
if status in ("failed", "timeout"):
|
|
373
|
+
item.error = chunk.chunk_metadata.get("error", chunk.text)
|
|
374
|
+
|
|
375
|
+
def _handle_annotation_chunk(self, chunk: Chunk) -> None:
|
|
376
|
+
"""Handle file annotation/citation chunks."""
|
|
377
|
+
if not self.messages:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
last_message = self.messages[-1]
|
|
381
|
+
if last_message.type != MessageType.ASSISTANT:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
# Extract annotation text (filename or source reference)
|
|
385
|
+
annotation_text = chunk.text
|
|
386
|
+
if annotation_text and annotation_text not in last_message.annotations:
|
|
387
|
+
last_message.annotations.append(annotation_text)
|
|
388
|
+
|
|
389
|
+
def _extract_citations_to_annotations(self, chunk: Chunk) -> None:
|
|
390
|
+
"""Extract citations from TEXT chunk metadata and add as annotations."""
|
|
391
|
+
citations_json = chunk.chunk_metadata.get("citations")
|
|
392
|
+
if not citations_json:
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
if not self.messages:
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
last_message = self.messages[-1]
|
|
399
|
+
if last_message.type != MessageType.ASSISTANT:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
citations = json.loads(citations_json)
|
|
404
|
+
except json.JSONDecodeError:
|
|
405
|
+
logger.warning("Failed to parse citations JSON: %s", citations_json)
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
max_citation_length = 50
|
|
409
|
+
for citation in citations:
|
|
410
|
+
# Prefer document_title, fall back to cited_text excerpt
|
|
411
|
+
annotation_text = citation.get("document_title")
|
|
412
|
+
if not annotation_text:
|
|
413
|
+
cited_text = citation.get("cited_text", "")
|
|
414
|
+
# Use first N chars of cited_text as fallback
|
|
415
|
+
if len(cited_text) > max_citation_length:
|
|
416
|
+
annotation_text = cited_text[:max_citation_length] + "..."
|
|
417
|
+
else:
|
|
418
|
+
annotation_text = cited_text
|
|
419
|
+
|
|
420
|
+
if annotation_text and annotation_text not in last_message.annotations:
|
|
421
|
+
last_message.annotations.append(annotation_text)
|
|
422
|
+
|
|
423
|
+
def _format_message_links(self) -> None:
|
|
424
|
+
"""Format consecutive markdown links in the last message as a bullet list.
|
|
425
|
+
|
|
426
|
+
This post-processes the accumulated message text to improve readability
|
|
427
|
+
when the LLM returns multiple links concatenated without proper spacing.
|
|
428
|
+
"""
|
|
429
|
+
if not self.messages:
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
last_message = self.messages[-1]
|
|
433
|
+
if last_message.type != MessageType.ASSISTANT:
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
if last_message.text:
|
|
437
|
+
last_message.text = _format_consecutive_links_as_list(last_message.text)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""System Prompt Builder service.
|
|
2
|
+
|
|
3
|
+
Provides unified system prompt construction with MCP tool injection
|
|
4
|
+
for all AI processors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from appkit_assistant.backend.system_prompt_cache import get_system_prompt
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SystemPromptBuilder:
|
|
15
|
+
"""Service for building system prompts with MCP tool injection."""
|
|
16
|
+
|
|
17
|
+
# Header for MCP tool selection guidelines
|
|
18
|
+
MCP_SECTION_HEADER = (
|
|
19
|
+
"### Tool-Auswahlrichtlinien (Einbettung externer Beschreibungen)"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def _build_mcp_section(self, mcp_prompt: str) -> str:
|
|
23
|
+
"""Build the MCP tool selection section.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
mcp_prompt: The MCP tool prompts from servers
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Formatted MCP section or empty string
|
|
30
|
+
"""
|
|
31
|
+
if not mcp_prompt:
|
|
32
|
+
return ""
|
|
33
|
+
return f"{self.MCP_SECTION_HEADER}\n{mcp_prompt}"
|
|
34
|
+
|
|
35
|
+
async def build(self, mcp_prompt: str = "") -> str:
|
|
36
|
+
"""Build the complete system prompt with MCP section.
|
|
37
|
+
|
|
38
|
+
Retrieves the base system prompt from cache and injects
|
|
39
|
+
the MCP tool prompts via the {mcp_prompts} placeholder.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
mcp_prompt: Optional MCP tool prompts from servers
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Complete formatted system prompt
|
|
46
|
+
"""
|
|
47
|
+
# Get base system prompt from cache
|
|
48
|
+
system_prompt_template = await get_system_prompt()
|
|
49
|
+
|
|
50
|
+
# Build MCP section
|
|
51
|
+
mcp_section = self._build_mcp_section(mcp_prompt)
|
|
52
|
+
|
|
53
|
+
# Format template with MCP prompts
|
|
54
|
+
return system_prompt_template.format(mcp_prompts=mcp_section)
|
|
55
|
+
|
|
56
|
+
async def build_with_prefix(
|
|
57
|
+
self,
|
|
58
|
+
mcp_prompt: str = "",
|
|
59
|
+
prefix: str = "",
|
|
60
|
+
) -> str:
|
|
61
|
+
"""Build system prompt with optional prefix.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
mcp_prompt: Optional MCP tool prompts
|
|
65
|
+
prefix: Optional prefix to prepend (e.g., for Gemini's leading newlines)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Complete formatted system prompt with prefix
|
|
69
|
+
"""
|
|
70
|
+
prompt = await self.build(mcp_prompt)
|
|
71
|
+
if prefix:
|
|
72
|
+
return f"{prefix}{prompt}"
|
|
73
|
+
return prompt
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Singleton instance
|
|
77
|
+
_system_prompt_builder: SystemPromptBuilder | None = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_system_prompt_builder() -> SystemPromptBuilder:
|
|
81
|
+
"""Get or create the system prompt builder singleton.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The SystemPromptBuilder instance
|
|
85
|
+
"""
|
|
86
|
+
global _system_prompt_builder
|
|
87
|
+
if _system_prompt_builder is None:
|
|
88
|
+
_system_prompt_builder = SystemPromptBuilder()
|
|
89
|
+
return _system_prompt_builder
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import uuid
|
|
3
3
|
|
|
4
|
-
from appkit_assistant.backend.
|
|
5
|
-
from appkit_assistant.backend.models import (
|
|
4
|
+
from appkit_assistant.backend.database.models import (
|
|
6
5
|
AssistantThread,
|
|
6
|
+
)
|
|
7
|
+
from appkit_assistant.backend.database.repositories import thread_repo
|
|
8
|
+
from appkit_assistant.backend.model_manager import ModelManager
|
|
9
|
+
from appkit_assistant.backend.schemas import (
|
|
7
10
|
Message,
|
|
8
11
|
ThreadModel,
|
|
9
12
|
ThreadStatus,
|
|
10
13
|
)
|
|
11
|
-
from appkit_assistant.backend.repositories import thread_repo
|
|
12
14
|
from appkit_commons.database.session import get_asyncdb_session
|
|
13
15
|
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
from datetime import UTC, datetime, timedelta
|
|
4
|
-
from typing import Final
|
|
4
|
+
from typing import Final, Self
|
|
5
5
|
|
|
6
|
-
from appkit_assistant.backend.repositories import system_prompt_repo
|
|
6
|
+
from appkit_assistant.backend.database.repositories import system_prompt_repo
|
|
7
7
|
from appkit_commons.database.session import get_asyncdb_session
|
|
8
8
|
|
|
9
9
|
logger = logging.getLogger(__name__)
|
|
@@ -25,7 +25,7 @@ class SystemPromptCache:
|
|
|
25
25
|
_instance: "SystemPromptCache | None" = None
|
|
26
26
|
_lock: asyncio.Lock = asyncio.Lock()
|
|
27
27
|
|
|
28
|
-
def __new__(cls) ->
|
|
28
|
+
def __new__(cls) -> Self:
|
|
29
29
|
"""Ensure singleton pattern."""
|
|
30
30
|
if cls._instance is None:
|
|
31
31
|
cls._instance = super().__new__(cls)
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
from appkit_assistant.backend.models import Suggestion
|
|
2
1
|
from appkit_assistant.components.composer import composer
|
|
2
|
+
from appkit_assistant.components.file_manager import file_manager
|
|
3
3
|
from appkit_assistant.components.thread import Assistant
|
|
4
4
|
from appkit_assistant.components.message import MessageComponent
|
|
5
|
-
from appkit_assistant.backend.models import (
|
|
5
|
+
from appkit_assistant.backend.database.models import (
|
|
6
|
+
MCPServer,
|
|
7
|
+
ThreadStatus,
|
|
8
|
+
)
|
|
9
|
+
from appkit_assistant.backend.schemas import (
|
|
6
10
|
AIModel,
|
|
7
11
|
Chunk,
|
|
8
12
|
ChunkType,
|
|
9
|
-
MCPServer,
|
|
10
13
|
Message,
|
|
11
14
|
MessageType,
|
|
15
|
+
Suggestion,
|
|
12
16
|
ThreadModel,
|
|
13
|
-
ThreadStatus,
|
|
14
17
|
UploadedFile,
|
|
15
18
|
)
|
|
16
19
|
from appkit_assistant.state.thread_list_state import ThreadListState
|
|
@@ -34,5 +37,6 @@ __all__ = [
|
|
|
34
37
|
"ThreadStatus",
|
|
35
38
|
"UploadedFile",
|
|
36
39
|
"composer",
|
|
40
|
+
"file_manager",
|
|
37
41
|
"mcp_servers_table",
|
|
38
42
|
]
|
|
@@ -3,7 +3,7 @@ from collections.abc import Callable
|
|
|
3
3
|
import reflex as rx
|
|
4
4
|
|
|
5
5
|
import appkit_mantine as mn
|
|
6
|
-
from appkit_assistant.backend.
|
|
6
|
+
from appkit_assistant.backend.schemas import UploadedFile
|
|
7
7
|
from appkit_assistant.components.tools_modal import tools_popover
|
|
8
8
|
from appkit_assistant.state.thread_state import ThreadState
|
|
9
9
|
|
|
@@ -67,11 +67,14 @@ def submit() -> rx.Component:
|
|
|
67
67
|
),
|
|
68
68
|
content="Stoppen",
|
|
69
69
|
),
|
|
70
|
-
rx.
|
|
71
|
-
rx.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
rx.tooltip(
|
|
71
|
+
rx.button(
|
|
72
|
+
rx.icon("arrow-right", size=18),
|
|
73
|
+
id="composer-submit",
|
|
74
|
+
name="composer_submit",
|
|
75
|
+
type="submit",
|
|
76
|
+
),
|
|
77
|
+
content="Absenden",
|
|
75
78
|
),
|
|
76
79
|
)
|
|
77
80
|
|
|
@@ -137,36 +140,41 @@ def file_upload(show: bool = False) -> rx.Component:
|
|
|
137
140
|
show & ThreadState.selected_model_supports_attachments,
|
|
138
141
|
rx.tooltip(
|
|
139
142
|
rx.upload.root(
|
|
140
|
-
rx.
|
|
141
|
-
rx.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
rx.tooltip(
|
|
144
|
+
rx.button(
|
|
145
|
+
rx.icon("paperclip", size=17),
|
|
146
|
+
cursor="pointer",
|
|
147
|
+
variant="ghost",
|
|
148
|
+
padding="8px",
|
|
149
|
+
),
|
|
150
|
+
content=(
|
|
151
|
+
"Dateien hochladen (max. "
|
|
152
|
+
f"{ThreadState.max_files_per_thread}, "
|
|
153
|
+
f"{ThreadState.max_file_size_mb}MB pro Datei)"
|
|
154
|
+
),
|
|
146
155
|
),
|
|
147
156
|
id="composer_file_upload",
|
|
148
157
|
accept={
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ # noqa: E501
|
|
159
|
+
".xlsx"
|
|
160
|
+
],
|
|
161
|
+
"text/csv": [".csv"],
|
|
162
|
+
"application/vnd.openxmlformats-officedocument."
|
|
163
|
+
"wordprocessingml.document": [".docx"],
|
|
164
|
+
"application/vnd.openxmlformats-officedocument."
|
|
165
|
+
"presentationml.presentation": [".pptx"],
|
|
166
|
+
"text/markdown": [".md"],
|
|
158
167
|
"application/pdf": [".pdf"],
|
|
159
168
|
"image/png": [".png"],
|
|
160
169
|
"image/jpeg": [".jpg", ".jpeg"],
|
|
161
170
|
},
|
|
162
171
|
multiple=True,
|
|
163
|
-
|
|
164
|
-
max_size=5 * 1024 * 1024,
|
|
172
|
+
max_size=ThreadState.max_file_size_mb * 1024 * 1024,
|
|
165
173
|
on_drop=ThreadState.handle_upload(
|
|
166
174
|
rx.upload_files(upload_id="composer_file_upload")
|
|
167
175
|
),
|
|
168
176
|
),
|
|
169
|
-
content="Dateien hochladen (max.
|
|
177
|
+
content=f"Dateien hochladen (max. {ThreadState.max_files_per_thread}, {ThreadState.max_file_size_mb}MB pro Datei)",
|
|
170
178
|
),
|
|
171
179
|
rx.fragment(),
|
|
172
180
|
)
|
|
@@ -200,11 +208,36 @@ def choose_model(show: bool = False) -> rx.Component | None:
|
|
|
200
208
|
)
|
|
201
209
|
|
|
202
210
|
|
|
211
|
+
def web_search_toggle() -> rx.Component:
|
|
212
|
+
"""Render web search toggle button."""
|
|
213
|
+
return rx.cond(
|
|
214
|
+
ThreadState.selected_model_supports_search,
|
|
215
|
+
rx.tooltip(
|
|
216
|
+
rx.button(
|
|
217
|
+
rx.icon("globe", size=17),
|
|
218
|
+
cursor="pointer",
|
|
219
|
+
variant=rx.cond(ThreadState.web_search_enabled, "solid", "ghost"),
|
|
220
|
+
color_scheme=rx.cond(ThreadState.web_search_enabled, "blue", "accent"),
|
|
221
|
+
padding="8px",
|
|
222
|
+
margin_right=rx.cond(
|
|
223
|
+
ThreadState.selected_model_supports_attachments, "6px", "14px"
|
|
224
|
+
),
|
|
225
|
+
margin_left="-6px",
|
|
226
|
+
on_click=ThreadState.toggle_web_search,
|
|
227
|
+
type="button",
|
|
228
|
+
),
|
|
229
|
+
content="Websuche aktivieren",
|
|
230
|
+
),
|
|
231
|
+
rx.fragment(),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
203
235
|
def tools(show: bool = False) -> rx.Component:
|
|
204
236
|
"""Render tools button with conditional visibility."""
|
|
205
237
|
return rx.cond(
|
|
206
238
|
show,
|
|
207
239
|
rx.hstack(
|
|
240
|
+
web_search_toggle(),
|
|
208
241
|
tools_popover(),
|
|
209
242
|
spacing="1",
|
|
210
243
|
align="center",
|
|
@@ -220,6 +253,7 @@ def clear(show: bool = True) -> rx.Component | None:
|
|
|
220
253
|
return rx.tooltip(
|
|
221
254
|
rx.button(
|
|
222
255
|
rx.icon("paintbrush", size=17),
|
|
256
|
+
cursor="pointer",
|
|
223
257
|
variant="ghost",
|
|
224
258
|
padding="8px",
|
|
225
259
|
on_click=ThreadState.clear,
|
|
@@ -244,6 +278,7 @@ class ComposerComponent(rx.ComponentNamespace):
|
|
|
244
278
|
choose_model = staticmethod(choose_model)
|
|
245
279
|
clear = staticmethod(clear)
|
|
246
280
|
file_upload = staticmethod(file_upload)
|
|
281
|
+
web_search_toggle = staticmethod(web_search_toggle)
|
|
247
282
|
input = staticmethod(composer_input)
|
|
248
283
|
selected_files_row = staticmethod(selected_files_row)
|
|
249
284
|
submit = staticmethod(submit)
|