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,76 +1,25 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import enum
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
4
|
from collections.abc import AsyncGenerator
|
|
6
5
|
from typing import Any
|
|
7
6
|
|
|
8
|
-
from
|
|
9
|
-
from appkit_assistant.backend.processors.openai_chat_completion_processor import (
|
|
10
|
-
OpenAIChatCompletionsProcessor,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class ContextSize(enum.StrEnum):
|
|
17
|
-
"""Enum for context size options."""
|
|
18
|
-
|
|
19
|
-
LOW = "low"
|
|
20
|
-
MEDIUM = "medium"
|
|
21
|
-
HIGH = "high"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class PerplexityAIModel(AIModel):
|
|
25
|
-
"""AI model for Perplexity API."""
|
|
7
|
+
from openai import AsyncStream
|
|
26
8
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
SONAR = PerplexityAIModel(
|
|
32
|
-
id="sonar",
|
|
33
|
-
text="Perplexity Sonar",
|
|
34
|
-
icon="perplexity",
|
|
35
|
-
model="sonar",
|
|
36
|
-
stream=True,
|
|
9
|
+
from appkit_assistant.backend.database.models import (
|
|
10
|
+
MCPServer,
|
|
37
11
|
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
text="Perplexity Sonar Pro",
|
|
42
|
-
icon="perplexity",
|
|
43
|
-
model="sonar-pro",
|
|
44
|
-
stream=True,
|
|
45
|
-
keywords=["sonar", "perplexity"],
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
SONAR_DEEP_RESEARCH = PerplexityAIModel(
|
|
49
|
-
id="sonar-deep-research",
|
|
50
|
-
text="Perplexity Deep Research",
|
|
51
|
-
icon="perplexity",
|
|
52
|
-
model="sonar-deep-research",
|
|
53
|
-
search_context_size=ContextSize.HIGH,
|
|
54
|
-
stream=True,
|
|
55
|
-
keywords=["reasoning", "deep", "research", "perplexity"],
|
|
12
|
+
from appkit_assistant.backend.models.perplexity import PerplexityAIModel
|
|
13
|
+
from appkit_assistant.backend.processors.openai_chat_completion_processor import (
|
|
14
|
+
OpenAIChatCompletionsProcessor,
|
|
56
15
|
)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
text="Perplexity Reasoning",
|
|
61
|
-
icon="perplexity",
|
|
62
|
-
model="sonar-reasoning",
|
|
63
|
-
search_context_size=ContextSize.HIGH,
|
|
64
|
-
stream=True,
|
|
65
|
-
keywords=["reasoning", "perplexity"],
|
|
16
|
+
from appkit_assistant.backend.schemas import (
|
|
17
|
+
Chunk,
|
|
18
|
+
Message,
|
|
66
19
|
)
|
|
20
|
+
from appkit_assistant.backend.services.chunk_factory import ChunkFactory
|
|
67
21
|
|
|
68
|
-
|
|
69
|
-
SONAR.id: SONAR,
|
|
70
|
-
SONAR_PRO.id: SONAR_PRO,
|
|
71
|
-
SONAR_DEEP_RESEARCH.id: SONAR_DEEP_RESEARCH,
|
|
72
|
-
SONAR_REASONING.id: SONAR_REASONING,
|
|
73
|
-
}
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
74
23
|
|
|
75
24
|
|
|
76
25
|
class PerplexityProcessor(OpenAIChatCompletionsProcessor):
|
|
@@ -83,24 +32,79 @@ class PerplexityProcessor(OpenAIChatCompletionsProcessor):
|
|
|
83
32
|
) -> None:
|
|
84
33
|
self.base_url = "https://api.perplexity.ai"
|
|
85
34
|
super().__init__(api_key=api_key, base_url=self.base_url, models=models)
|
|
35
|
+
self._chunk_factory = ChunkFactory(processor_name="perplexity")
|
|
86
36
|
|
|
87
37
|
async def process(
|
|
88
38
|
self,
|
|
89
39
|
messages: list[Message],
|
|
90
40
|
model_id: str,
|
|
91
|
-
files: list[str] | None = None,
|
|
92
|
-
mcp_servers: list[MCPServer] | None = None,
|
|
41
|
+
files: list[str] | None = None, # noqa: ARG002
|
|
42
|
+
mcp_servers: list[MCPServer] | None = None,
|
|
93
43
|
payload: dict[str, Any] | None = None,
|
|
94
44
|
cancellation_token: asyncio.Event | None = None,
|
|
95
|
-
**kwargs: Any,
|
|
45
|
+
**kwargs: Any, # noqa: ARG002
|
|
96
46
|
) -> AsyncGenerator[Chunk, None]:
|
|
47
|
+
"""Process messages using Perplexity API with citation support.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
messages: List of messages to process.
|
|
51
|
+
model_id: ID of the model to use.
|
|
52
|
+
files: File attachments (not used in Perplexity).
|
|
53
|
+
mcp_servers: MCP servers (not supported, will log warning).
|
|
54
|
+
payload: Additional payload parameters.
|
|
55
|
+
cancellation_token: Optional event to signal cancellation.
|
|
56
|
+
**kwargs: Additional arguments.
|
|
57
|
+
"""
|
|
58
|
+
if not self.client:
|
|
59
|
+
raise ValueError("Perplexity Client not initialized.")
|
|
60
|
+
|
|
97
61
|
if model_id not in self.models:
|
|
98
62
|
logger.error("Model %s not supported by Perplexity processor", model_id)
|
|
99
63
|
raise ValueError(f"Model {model_id} not supported by Perplexity processor")
|
|
100
64
|
|
|
101
|
-
|
|
65
|
+
if mcp_servers:
|
|
66
|
+
logger.warning(
|
|
67
|
+
"MCP servers provided to PerplexityProcessor but not supported."
|
|
68
|
+
)
|
|
102
69
|
|
|
103
|
-
|
|
70
|
+
model = self.models[model_id]
|
|
71
|
+
perplexity_payload = self._build_perplexity_payload(model, payload)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
chat_messages = self._convert_messages_to_openai_format(messages)
|
|
75
|
+
session = await self.client.chat.completions.create(
|
|
76
|
+
model=model.model,
|
|
77
|
+
messages=chat_messages[:-1],
|
|
78
|
+
stream=model.stream,
|
|
79
|
+
temperature=model.temperature,
|
|
80
|
+
extra_body=perplexity_payload,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if isinstance(session, AsyncStream):
|
|
84
|
+
async for chunk in self._process_streaming_response(
|
|
85
|
+
session, model, cancellation_token
|
|
86
|
+
):
|
|
87
|
+
yield chunk
|
|
88
|
+
else:
|
|
89
|
+
async for chunk in self._process_non_streaming_response(session, model):
|
|
90
|
+
yield chunk
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error("Error in Perplexity processor: %s", e)
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
def _build_perplexity_payload(
|
|
97
|
+
self, model: PerplexityAIModel, payload: dict[str, Any] | None
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
"""Build the Perplexity-specific payload.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
model: The Perplexity AI model configuration.
|
|
103
|
+
payload: Additional payload parameters to merge.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Combined payload dictionary for the API request.
|
|
107
|
+
"""
|
|
104
108
|
perplexity_payload = {
|
|
105
109
|
"search_domain_filter": model.search_domain_filter,
|
|
106
110
|
"return_images": True,
|
|
@@ -109,18 +113,91 @@ class PerplexityProcessor(OpenAIChatCompletionsProcessor):
|
|
|
109
113
|
"search_context_size": model.search_context_size,
|
|
110
114
|
},
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
# Merge with any additional payload
|
|
114
116
|
if payload:
|
|
115
117
|
perplexity_payload.update(payload)
|
|
118
|
+
return perplexity_payload
|
|
119
|
+
|
|
120
|
+
async def _process_streaming_response(
|
|
121
|
+
self,
|
|
122
|
+
session: AsyncStream[Any],
|
|
123
|
+
model: PerplexityAIModel,
|
|
124
|
+
cancellation_token: asyncio.Event | None,
|
|
125
|
+
) -> AsyncGenerator[Chunk, None]:
|
|
126
|
+
"""Process a streaming response from Perplexity API.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
session: The async stream from the API.
|
|
130
|
+
model: The model configuration.
|
|
131
|
+
cancellation_token: Optional cancellation event.
|
|
132
|
+
|
|
133
|
+
Yields:
|
|
134
|
+
Chunk objects with text content and citations.
|
|
135
|
+
"""
|
|
136
|
+
citations: list[str] = []
|
|
137
|
+
|
|
138
|
+
async for event in session:
|
|
139
|
+
if cancellation_token and cancellation_token.is_set():
|
|
140
|
+
logger.info("Processing cancelled by user")
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
# Extract citations from streaming response if available
|
|
144
|
+
if hasattr(event, "citations") and event.citations:
|
|
145
|
+
citations = event.citations
|
|
146
|
+
|
|
147
|
+
if event.choices and event.choices[0].delta:
|
|
148
|
+
content = event.choices[0].delta.content
|
|
149
|
+
if content:
|
|
150
|
+
yield self._create_chunk(
|
|
151
|
+
content, model.model, stream=True, message_id=event.id
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# After streaming completes, yield citation annotations
|
|
155
|
+
async for chunk in self._yield_citations(citations):
|
|
156
|
+
yield chunk
|
|
157
|
+
|
|
158
|
+
async def _process_non_streaming_response(
|
|
159
|
+
self, session: Any, model: PerplexityAIModel
|
|
160
|
+
) -> AsyncGenerator[Chunk, None]:
|
|
161
|
+
"""Process a non-streaming response from Perplexity API.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
session: The response object from the API.
|
|
165
|
+
model: The model configuration.
|
|
166
|
+
|
|
167
|
+
Yields:
|
|
168
|
+
Chunk objects with text content and citations.
|
|
169
|
+
"""
|
|
170
|
+
content = session.choices[0].message.content
|
|
171
|
+
citations: list[str] = []
|
|
172
|
+
|
|
173
|
+
if hasattr(session, "citations") and session.citations:
|
|
174
|
+
citations = session.citations
|
|
175
|
+
|
|
176
|
+
if content:
|
|
177
|
+
yield self._create_chunk(content, model.model, message_id=session.id)
|
|
178
|
+
|
|
179
|
+
async for chunk in self._yield_citations(citations):
|
|
180
|
+
yield chunk
|
|
181
|
+
|
|
182
|
+
async def _yield_citations(
|
|
183
|
+
self, citations: list[str]
|
|
184
|
+
) -> AsyncGenerator[Chunk, None]:
|
|
185
|
+
"""Yield annotation chunks for citations.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
citations: List of citation URLs from Perplexity API.
|
|
189
|
+
|
|
190
|
+
Yields:
|
|
191
|
+
Chunk objects with citation annotations.
|
|
192
|
+
"""
|
|
193
|
+
if not citations:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
logger.debug("Processing %d citations from Perplexity", len(citations))
|
|
116
197
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
cancellation_token=cancellation_token,
|
|
124
|
-
**kwargs,
|
|
125
|
-
):
|
|
126
|
-
yield response
|
|
198
|
+
# Yield individual ANNOTATION chunks for citations display
|
|
199
|
+
for url in citations:
|
|
200
|
+
yield self._chunk_factory.annotation(
|
|
201
|
+
text=url,
|
|
202
|
+
annotation_data={"url": url, "source": "perplexity"},
|
|
203
|
+
)
|
|
@@ -7,7 +7,12 @@ import asyncio
|
|
|
7
7
|
import logging
|
|
8
8
|
from collections.abc import AsyncGenerator
|
|
9
9
|
|
|
10
|
-
from appkit_assistant.backend.models import
|
|
10
|
+
from appkit_assistant.backend.database.models import MCPServer
|
|
11
|
+
from appkit_assistant.backend.schemas import (
|
|
12
|
+
AIModel,
|
|
13
|
+
Chunk,
|
|
14
|
+
Message,
|
|
15
|
+
)
|
|
11
16
|
from appkit_commons.configuration.configuration import ReflexConfig
|
|
12
17
|
from appkit_commons.registry import service_registry
|
|
13
18
|
|
|
@@ -31,7 +36,7 @@ def mcp_oauth_redirect_uri() -> str:
|
|
|
31
36
|
return f"http://localhost:8080{MCP_OAUTH_CALLBACK_PATH}"
|
|
32
37
|
|
|
33
38
|
|
|
34
|
-
class
|
|
39
|
+
class ProcessorBase(abc.ABC):
|
|
35
40
|
"""Base processor interface for AI processing services."""
|
|
36
41
|
|
|
37
42
|
@abc.abstractmethod
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Streaming Processor Base class.
|
|
2
|
+
|
|
3
|
+
Provides a unified base class for all streaming AI processors with:
|
|
4
|
+
- Standardized dict-based event dispatch
|
|
5
|
+
- Cancellation token handling
|
|
6
|
+
- Common state management
|
|
7
|
+
- Composed service dependencies
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from collections.abc import AsyncGenerator
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from appkit_assistant.backend.database.models import (
|
|
17
|
+
MCPServer,
|
|
18
|
+
)
|
|
19
|
+
from appkit_assistant.backend.processors.processor_base import ProcessorBase
|
|
20
|
+
from appkit_assistant.backend.schemas import (
|
|
21
|
+
AIModel,
|
|
22
|
+
Chunk,
|
|
23
|
+
ChunkType,
|
|
24
|
+
Message,
|
|
25
|
+
)
|
|
26
|
+
from appkit_assistant.backend.services.auth_error_detector import AuthErrorDetector
|
|
27
|
+
from appkit_assistant.backend.services.chunk_factory import ChunkFactory
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class StreamingProcessorBase(ProcessorBase, ABC):
|
|
33
|
+
"""Base class for streaming AI processors.
|
|
34
|
+
|
|
35
|
+
Provides common functionality for:
|
|
36
|
+
- Event dispatch via dict-based handlers
|
|
37
|
+
- Cancellation token checking
|
|
38
|
+
- Common state tracking (reasoning sessions)
|
|
39
|
+
- Chunk creation via ChunkFactory
|
|
40
|
+
- Error detection via AuthErrorDetector
|
|
41
|
+
|
|
42
|
+
Note: For user ID tracking and MCP capabilities, combine with MCPCapabilities mixin.
|
|
43
|
+
|
|
44
|
+
Subclasses must implement:
|
|
45
|
+
- _get_event_handlers(): Return dict mapping event types to handlers
|
|
46
|
+
- get_supported_models(): Return supported models
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
models: dict[str, AIModel],
|
|
52
|
+
processor_name: str,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Initialize the streaming processor base.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
models: Dictionary of supported AI models
|
|
58
|
+
processor_name: Name for chunk metadata (e.g., "claude_responses")
|
|
59
|
+
"""
|
|
60
|
+
self.models = models
|
|
61
|
+
self._chunk_factory = ChunkFactory(processor_name)
|
|
62
|
+
self._auth_detector = AuthErrorDetector()
|
|
63
|
+
|
|
64
|
+
# Common streaming state
|
|
65
|
+
self._current_reasoning_session: str | None = None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def chunk_factory(self) -> ChunkFactory:
|
|
69
|
+
"""Get the chunk factory instance."""
|
|
70
|
+
return self._chunk_factory
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def auth_detector(self) -> AuthErrorDetector:
|
|
74
|
+
"""Get the auth error detector instance."""
|
|
75
|
+
return self._auth_detector
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def current_reasoning_session(self) -> str | None:
|
|
79
|
+
"""Get the current reasoning session ID."""
|
|
80
|
+
return self._current_reasoning_session
|
|
81
|
+
|
|
82
|
+
@current_reasoning_session.setter
|
|
83
|
+
def current_reasoning_session(self, value: str | None) -> None:
|
|
84
|
+
"""Set the current reasoning session ID."""
|
|
85
|
+
self._current_reasoning_session = value
|
|
86
|
+
|
|
87
|
+
def get_supported_models(self) -> dict[str, AIModel]:
|
|
88
|
+
"""Return supported models."""
|
|
89
|
+
return self.models
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def _get_event_handlers(self) -> dict[str, Any]:
|
|
93
|
+
"""Get the event handler mapping.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Dict mapping event type strings to handler methods.
|
|
97
|
+
Handler methods should accept the event and return Chunk | None.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def _handle_event(self, event: Any) -> Chunk | None:
|
|
101
|
+
"""Handle a streaming event using dict-based dispatch.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
event: The event object from the API stream
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A Chunk if the event produced content, None otherwise
|
|
108
|
+
"""
|
|
109
|
+
event_type = getattr(event, "type", None)
|
|
110
|
+
if not event_type:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
handlers = self._get_event_handlers()
|
|
114
|
+
handler = handlers.get(event_type)
|
|
115
|
+
|
|
116
|
+
if handler:
|
|
117
|
+
return handler(event)
|
|
118
|
+
|
|
119
|
+
logger.debug("Unhandled event type: %s", event_type)
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
async def _process_stream_with_cancellation(
|
|
123
|
+
self,
|
|
124
|
+
stream: Any,
|
|
125
|
+
cancellation_token: asyncio.Event | None = None,
|
|
126
|
+
) -> AsyncGenerator[Chunk, None]:
|
|
127
|
+
"""Process a stream with cancellation support.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
stream: Async iterable stream of events
|
|
131
|
+
cancellation_token: Optional event to signal cancellation
|
|
132
|
+
|
|
133
|
+
Yields:
|
|
134
|
+
Chunk objects from handled events
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
async for event in stream:
|
|
138
|
+
if cancellation_token and cancellation_token.is_set():
|
|
139
|
+
logger.info("Processing cancelled by user")
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
chunk = self._handle_event(event)
|
|
143
|
+
if chunk:
|
|
144
|
+
yield chunk
|
|
145
|
+
except Exception as e:
|
|
146
|
+
error_msg = str(e)
|
|
147
|
+
logger.error("Error during stream processing: %s", error_msg)
|
|
148
|
+
|
|
149
|
+
# Only yield error chunk if NOT an auth error
|
|
150
|
+
if not self._auth_detector.is_auth_error(error_msg):
|
|
151
|
+
yield self._chunk_factory.error(
|
|
152
|
+
f"Ein Fehler ist aufgetreten: {error_msg}",
|
|
153
|
+
error_type=type(e).__name__,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _create_chunk(
|
|
157
|
+
self,
|
|
158
|
+
chunk_type: ChunkType,
|
|
159
|
+
content: str,
|
|
160
|
+
extra_metadata: dict[str, Any] | None = None,
|
|
161
|
+
) -> Chunk:
|
|
162
|
+
"""Create a chunk using the factory (convenience method).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
chunk_type: The type of chunk
|
|
166
|
+
content: The text content
|
|
167
|
+
extra_metadata: Additional metadata
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
A Chunk instance
|
|
171
|
+
"""
|
|
172
|
+
return self._chunk_factory.create(chunk_type, content, extra_metadata)
|
|
173
|
+
|
|
174
|
+
@abstractmethod
|
|
175
|
+
async def process(
|
|
176
|
+
self,
|
|
177
|
+
messages: list[Message],
|
|
178
|
+
model_id: str,
|
|
179
|
+
files: list[str] | None = None,
|
|
180
|
+
mcp_servers: list[MCPServer] | None = None,
|
|
181
|
+
payload: dict[str, Any] | None = None,
|
|
182
|
+
user_id: int | None = None,
|
|
183
|
+
cancellation_token: asyncio.Event | None = None,
|
|
184
|
+
) -> AsyncGenerator[Chunk, None]:
|
|
185
|
+
"""Process messages and generate response chunks.
|
|
186
|
+
|
|
187
|
+
Must be implemented by subclasses to handle vendor-specific API calls.
|
|
188
|
+
"""
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from sqlmodel import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ChunkType(StrEnum):
|
|
9
|
+
"""Enum for chunk types."""
|
|
10
|
+
|
|
11
|
+
TEXT = "text" # default
|
|
12
|
+
ANNOTATION = "annotation" # for text annotations
|
|
13
|
+
IMAGE = "image"
|
|
14
|
+
IMAGE_PARTIAL = "image_partial" # for streaming image generation
|
|
15
|
+
THINKING = "thinking" # when the model is "thinking" / reasoning
|
|
16
|
+
THINKING_RESULT = "thinking_result" # when the "thinking" is done
|
|
17
|
+
ACTION = "action" # when the user needs to take action
|
|
18
|
+
TOOL_RESULT = "tool_result" # result from a tool
|
|
19
|
+
TOOL_CALL = "tool_call" # calling a tool
|
|
20
|
+
PROCESSING = "processing" # file processing status
|
|
21
|
+
COMPLETION = "completion" # when response generation is complete
|
|
22
|
+
AUTH_REQUIRED = "auth_required" # user needs to authenticate (MCP)
|
|
23
|
+
ERROR = "error" # when an error occurs
|
|
24
|
+
LIFECYCLE = "lifecycle"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Chunk(BaseModel):
|
|
28
|
+
"""Model for text chunks."""
|
|
29
|
+
|
|
30
|
+
type: ChunkType
|
|
31
|
+
text: str
|
|
32
|
+
chunk_metadata: dict[str, str | None] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ThreadStatus(StrEnum):
|
|
36
|
+
"""Enum for thread status."""
|
|
37
|
+
|
|
38
|
+
NEW = "new"
|
|
39
|
+
ACTIVE = "active"
|
|
40
|
+
IDLE = "idle"
|
|
41
|
+
WAITING = "waiting"
|
|
42
|
+
ERROR = "error"
|
|
43
|
+
DELETED = "deleted"
|
|
44
|
+
ARCHIVED = "archived"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MessageType(StrEnum):
|
|
48
|
+
"""Enum for message types."""
|
|
49
|
+
|
|
50
|
+
HUMAN = "human"
|
|
51
|
+
SYSTEM = "system"
|
|
52
|
+
ASSISTANT = "assistant"
|
|
53
|
+
TOOL_USE = "tool_use"
|
|
54
|
+
ERROR = "error"
|
|
55
|
+
INFO = "info"
|
|
56
|
+
WARNING = "warning"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Message(BaseModel):
|
|
60
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
61
|
+
text: str
|
|
62
|
+
original_text: str | None = None # To store original text if edited
|
|
63
|
+
editable: bool = False
|
|
64
|
+
type: MessageType
|
|
65
|
+
done: bool = False
|
|
66
|
+
attachments: list[str] = [] # List of filenames for display
|
|
67
|
+
annotations: list[str] = [] # List of file citations/annotations
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ThinkingType(StrEnum):
|
|
71
|
+
REASONING = "reasoning"
|
|
72
|
+
TOOL_CALL = "tool_call"
|
|
73
|
+
PROCESSING = "processing"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ThinkingStatus(StrEnum):
|
|
77
|
+
IN_PROGRESS = "in_progress"
|
|
78
|
+
COMPLETED = "completed"
|
|
79
|
+
ERROR = "error"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Thinking(BaseModel):
|
|
83
|
+
type: ThinkingType
|
|
84
|
+
id: str # reasoning_session_id or tool_id
|
|
85
|
+
text: str
|
|
86
|
+
status: ThinkingStatus = ThinkingStatus.IN_PROGRESS
|
|
87
|
+
tool_name: str | None = None
|
|
88
|
+
parameters: str | None = None
|
|
89
|
+
result: str | None = None
|
|
90
|
+
error: str | None = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class AIModel(BaseModel):
|
|
94
|
+
id: str
|
|
95
|
+
text: str
|
|
96
|
+
icon: str = "codesandbox"
|
|
97
|
+
stream: bool = False
|
|
98
|
+
tenant_key: str = ""
|
|
99
|
+
project_id: int = 0
|
|
100
|
+
model: str = "default"
|
|
101
|
+
temperature: float = 0.05
|
|
102
|
+
supports_tools: bool = False
|
|
103
|
+
supports_attachments: bool = False
|
|
104
|
+
supports_search: bool = False
|
|
105
|
+
keywords: list[str] = []
|
|
106
|
+
disabled: bool = False
|
|
107
|
+
requires_role: str | None = None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class Suggestion(BaseModel):
|
|
111
|
+
prompt: str
|
|
112
|
+
icon: str = ""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class UploadedFile(BaseModel):
|
|
116
|
+
"""Model for tracking uploaded files in the composer."""
|
|
117
|
+
|
|
118
|
+
filename: str
|
|
119
|
+
file_path: str
|
|
120
|
+
size: int = 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class ThreadModel(BaseModel):
|
|
124
|
+
thread_id: str
|
|
125
|
+
title: str = ""
|
|
126
|
+
active: bool = False
|
|
127
|
+
state: ThreadStatus = ThreadStatus.NEW
|
|
128
|
+
prompt: str | None = ""
|
|
129
|
+
messages: list[Message] = []
|
|
130
|
+
ai_model: str = ""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class MCPAuthType(StrEnum):
|
|
134
|
+
"""Enum for MCP server authentication types."""
|
|
135
|
+
|
|
136
|
+
NONE = "none"
|
|
137
|
+
API_KEY = "api_key"
|
|
138
|
+
OAUTH_DISCOVERY = "oauth_discovery"
|