spaik-sdk 0.6.2__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.
- spaik_sdk/__init__.py +21 -0
- spaik_sdk/agent/__init__.py +0 -0
- spaik_sdk/agent/base_agent.py +249 -0
- spaik_sdk/attachments/__init__.py +22 -0
- spaik_sdk/attachments/builder.py +61 -0
- spaik_sdk/attachments/file_storage_provider.py +27 -0
- spaik_sdk/attachments/mime_types.py +118 -0
- spaik_sdk/attachments/models.py +63 -0
- spaik_sdk/attachments/provider_support.py +53 -0
- spaik_sdk/attachments/storage/__init__.py +0 -0
- spaik_sdk/attachments/storage/base_file_storage.py +32 -0
- spaik_sdk/attachments/storage/impl/__init__.py +0 -0
- spaik_sdk/attachments/storage/impl/local_file_storage.py +101 -0
- spaik_sdk/audio/__init__.py +12 -0
- spaik_sdk/audio/options.py +53 -0
- spaik_sdk/audio/providers/__init__.py +1 -0
- spaik_sdk/audio/providers/google_tts.py +77 -0
- spaik_sdk/audio/providers/openai_stt.py +71 -0
- spaik_sdk/audio/providers/openai_tts.py +111 -0
- spaik_sdk/audio/stt.py +61 -0
- spaik_sdk/audio/tts.py +124 -0
- spaik_sdk/config/credentials_provider.py +10 -0
- spaik_sdk/config/env.py +59 -0
- spaik_sdk/config/env_credentials_provider.py +7 -0
- spaik_sdk/config/get_credentials_provider.py +14 -0
- spaik_sdk/image_gen/__init__.py +9 -0
- spaik_sdk/image_gen/image_generator.py +83 -0
- spaik_sdk/image_gen/options.py +24 -0
- spaik_sdk/image_gen/providers/__init__.py +0 -0
- spaik_sdk/image_gen/providers/google.py +75 -0
- spaik_sdk/image_gen/providers/openai.py +60 -0
- spaik_sdk/llm/__init__.py +0 -0
- spaik_sdk/llm/cancellation_handle.py +10 -0
- spaik_sdk/llm/consumption/__init__.py +0 -0
- spaik_sdk/llm/consumption/consumption_estimate.py +26 -0
- spaik_sdk/llm/consumption/consumption_estimate_builder.py +113 -0
- spaik_sdk/llm/consumption/consumption_extractor.py +59 -0
- spaik_sdk/llm/consumption/token_usage.py +31 -0
- spaik_sdk/llm/converters.py +146 -0
- spaik_sdk/llm/cost/__init__.py +1 -0
- spaik_sdk/llm/cost/builtin_cost_provider.py +83 -0
- spaik_sdk/llm/cost/cost_estimate.py +8 -0
- spaik_sdk/llm/cost/cost_provider.py +28 -0
- spaik_sdk/llm/extract_error_message.py +37 -0
- spaik_sdk/llm/langchain_loop_manager.py +270 -0
- spaik_sdk/llm/langchain_service.py +196 -0
- spaik_sdk/llm/message_handler.py +188 -0
- spaik_sdk/llm/streaming/__init__.py +1 -0
- spaik_sdk/llm/streaming/block_manager.py +152 -0
- spaik_sdk/llm/streaming/models.py +42 -0
- spaik_sdk/llm/streaming/streaming_content_handler.py +157 -0
- spaik_sdk/llm/streaming/streaming_event_handler.py +215 -0
- spaik_sdk/llm/streaming/streaming_state_manager.py +58 -0
- spaik_sdk/models/__init__.py +0 -0
- spaik_sdk/models/factories/__init__.py +0 -0
- spaik_sdk/models/factories/anthropic_factory.py +33 -0
- spaik_sdk/models/factories/base_model_factory.py +71 -0
- spaik_sdk/models/factories/google_factory.py +30 -0
- spaik_sdk/models/factories/ollama_factory.py +41 -0
- spaik_sdk/models/factories/openai_factory.py +50 -0
- spaik_sdk/models/llm_config.py +46 -0
- spaik_sdk/models/llm_families.py +7 -0
- spaik_sdk/models/llm_model.py +17 -0
- spaik_sdk/models/llm_wrapper.py +25 -0
- spaik_sdk/models/model_registry.py +156 -0
- spaik_sdk/models/providers/__init__.py +0 -0
- spaik_sdk/models/providers/anthropic_provider.py +29 -0
- spaik_sdk/models/providers/azure_provider.py +31 -0
- spaik_sdk/models/providers/base_provider.py +62 -0
- spaik_sdk/models/providers/google_provider.py +26 -0
- spaik_sdk/models/providers/ollama_provider.py +26 -0
- spaik_sdk/models/providers/openai_provider.py +26 -0
- spaik_sdk/models/providers/provider_type.py +90 -0
- spaik_sdk/orchestration/__init__.py +24 -0
- spaik_sdk/orchestration/base_orchestrator.py +238 -0
- spaik_sdk/orchestration/checkpoint.py +80 -0
- spaik_sdk/orchestration/models.py +103 -0
- spaik_sdk/prompt/__init__.py +0 -0
- spaik_sdk/prompt/get_prompt_loader.py +13 -0
- spaik_sdk/prompt/local_prompt_loader.py +21 -0
- spaik_sdk/prompt/prompt_loader.py +48 -0
- spaik_sdk/prompt/prompt_loader_mode.py +14 -0
- spaik_sdk/py.typed +1 -0
- spaik_sdk/recording/__init__.py +1 -0
- spaik_sdk/recording/base_playback.py +90 -0
- spaik_sdk/recording/base_recorder.py +50 -0
- spaik_sdk/recording/conditional_recorder.py +38 -0
- spaik_sdk/recording/impl/__init__.py +1 -0
- spaik_sdk/recording/impl/local_playback.py +76 -0
- spaik_sdk/recording/impl/local_recorder.py +85 -0
- spaik_sdk/recording/langchain_serializer.py +88 -0
- spaik_sdk/server/__init__.py +1 -0
- spaik_sdk/server/api/routers/__init__.py +0 -0
- spaik_sdk/server/api/routers/api_builder.py +149 -0
- spaik_sdk/server/api/routers/audio_router_factory.py +201 -0
- spaik_sdk/server/api/routers/file_router_factory.py +111 -0
- spaik_sdk/server/api/routers/thread_router_factory.py +284 -0
- spaik_sdk/server/api/streaming/__init__.py +0 -0
- spaik_sdk/server/api/streaming/format_sse_event.py +41 -0
- spaik_sdk/server/api/streaming/negotiate_streaming_response.py +8 -0
- spaik_sdk/server/api/streaming/streaming_negotiator.py +10 -0
- spaik_sdk/server/authorization/__init__.py +0 -0
- spaik_sdk/server/authorization/base_authorizer.py +64 -0
- spaik_sdk/server/authorization/base_user.py +13 -0
- spaik_sdk/server/authorization/dummy_authorizer.py +17 -0
- spaik_sdk/server/job_processor/__init__.py +0 -0
- spaik_sdk/server/job_processor/base_job_processor.py +8 -0
- spaik_sdk/server/job_processor/thread_job_processor.py +32 -0
- spaik_sdk/server/pubsub/__init__.py +1 -0
- spaik_sdk/server/pubsub/cancellation_publisher.py +7 -0
- spaik_sdk/server/pubsub/cancellation_subscriber.py +38 -0
- spaik_sdk/server/pubsub/event_publisher.py +13 -0
- spaik_sdk/server/pubsub/impl/__init__.py +1 -0
- spaik_sdk/server/pubsub/impl/local_cancellation_pubsub.py +48 -0
- spaik_sdk/server/pubsub/impl/signalr_publisher.py +36 -0
- spaik_sdk/server/queue/__init__.py +1 -0
- spaik_sdk/server/queue/agent_job_queue.py +27 -0
- spaik_sdk/server/queue/impl/__init__.py +1 -0
- spaik_sdk/server/queue/impl/azure_queue.py +24 -0
- spaik_sdk/server/response/__init__.py +0 -0
- spaik_sdk/server/response/agent_response_generator.py +39 -0
- spaik_sdk/server/response/response_generator.py +13 -0
- spaik_sdk/server/response/simple_agent_response_generator.py +14 -0
- spaik_sdk/server/services/__init__.py +0 -0
- spaik_sdk/server/services/thread_converters.py +113 -0
- spaik_sdk/server/services/thread_models.py +90 -0
- spaik_sdk/server/services/thread_service.py +91 -0
- spaik_sdk/server/storage/__init__.py +1 -0
- spaik_sdk/server/storage/base_thread_repository.py +51 -0
- spaik_sdk/server/storage/impl/__init__.py +0 -0
- spaik_sdk/server/storage/impl/in_memory_thread_repository.py +100 -0
- spaik_sdk/server/storage/impl/local_file_thread_repository.py +217 -0
- spaik_sdk/server/storage/thread_filter.py +166 -0
- spaik_sdk/server/storage/thread_metadata.py +53 -0
- spaik_sdk/thread/__init__.py +0 -0
- spaik_sdk/thread/adapters/__init__.py +0 -0
- spaik_sdk/thread/adapters/cli/__init__.py +0 -0
- spaik_sdk/thread/adapters/cli/block_display.py +92 -0
- spaik_sdk/thread/adapters/cli/display_manager.py +84 -0
- spaik_sdk/thread/adapters/cli/live_cli.py +235 -0
- spaik_sdk/thread/adapters/event_adapter.py +28 -0
- spaik_sdk/thread/adapters/streaming_block_adapter.py +57 -0
- spaik_sdk/thread/adapters/sync_adapter.py +76 -0
- spaik_sdk/thread/models.py +224 -0
- spaik_sdk/thread/thread_container.py +468 -0
- spaik_sdk/tools/__init__.py +0 -0
- spaik_sdk/tools/impl/__init__.py +0 -0
- spaik_sdk/tools/impl/mcp_tool_provider.py +93 -0
- spaik_sdk/tools/impl/search_tool_provider.py +18 -0
- spaik_sdk/tools/tool_provider.py +131 -0
- spaik_sdk/tracing/__init__.py +13 -0
- spaik_sdk/tracing/agent_trace.py +72 -0
- spaik_sdk/tracing/get_trace_sink.py +15 -0
- spaik_sdk/tracing/local_trace_sink.py +23 -0
- spaik_sdk/tracing/trace_sink.py +19 -0
- spaik_sdk/tracing/trace_sink_mode.py +14 -0
- spaik_sdk/utils/__init__.py +0 -0
- spaik_sdk/utils/init_logger.py +24 -0
- spaik_sdk-0.6.2.dist-info/METADATA +379 -0
- spaik_sdk-0.6.2.dist-info/RECORD +161 -0
- spaik_sdk-0.6.2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from spaik_sdk.server.pubsub.cancellation_publisher import CancellationPublisher
|
|
2
|
+
from spaik_sdk.server.pubsub.cancellation_subscriber import CancellationSubscriber
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class LocalCancellationPubsub:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self.publisher = LocalCancellationPublisher()
|
|
8
|
+
|
|
9
|
+
def create_subscriber(self, id: str) -> CancellationSubscriber:
|
|
10
|
+
return LocalCancellationSubscriber(id, self.publisher)
|
|
11
|
+
|
|
12
|
+
def get_publisher(self) -> CancellationPublisher:
|
|
13
|
+
return self.publisher
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LocalCancellationSubscriber(CancellationSubscriber):
|
|
17
|
+
def __init__(self, id: str, publisher: "LocalCancellationPublisher"):
|
|
18
|
+
self.publisher = publisher
|
|
19
|
+
super().__init__(id)
|
|
20
|
+
|
|
21
|
+
def _subscribe_to_cancellation(self) -> None:
|
|
22
|
+
self.publisher.subscribe(self)
|
|
23
|
+
|
|
24
|
+
def _unsubscribe_from_cancellation(self) -> None:
|
|
25
|
+
self.publisher.unsubscribe(self)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class LocalCancellationPublisher(CancellationPublisher):
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.subscribers: list[CancellationSubscriber] = []
|
|
31
|
+
|
|
32
|
+
def subscribe(self, subscriber: CancellationSubscriber) -> None:
|
|
33
|
+
self.subscribers.append(subscriber)
|
|
34
|
+
|
|
35
|
+
def unsubscribe(self, subscriber: CancellationSubscriber) -> None:
|
|
36
|
+
self.subscribers.remove(subscriber)
|
|
37
|
+
|
|
38
|
+
def publish_cancellation(self, id: str) -> None:
|
|
39
|
+
for subscriber in self.subscribers:
|
|
40
|
+
if subscriber.id == id:
|
|
41
|
+
subscriber.on_cancellation()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
local_cancellation_pubsub = LocalCancellationPubsub()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_local_cancellation_pubsub() -> LocalCancellationPubsub:
|
|
48
|
+
return local_cancellation_pubsub
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.utils.init_logger import init_logger
|
|
5
|
+
|
|
6
|
+
logger = init_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# TODO turn into proper events, consider taking it to a different azure lib
|
|
10
|
+
class SignalRPublisher:
|
|
11
|
+
"""Azure SignalR publisher"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, config, signalr_out):
|
|
14
|
+
"""
|
|
15
|
+
Args:
|
|
16
|
+
signalr_out: Azure Functions SignalR output binding (func.Out[str])
|
|
17
|
+
"""
|
|
18
|
+
# super().__init__(config)
|
|
19
|
+
self.signalr_out = signalr_out
|
|
20
|
+
|
|
21
|
+
def publish_event(self, event: Dict[str, Any], job_id: str) -> None:
|
|
22
|
+
"""Publish event to Azure SignalR"""
|
|
23
|
+
|
|
24
|
+
# SignalR message format
|
|
25
|
+
signalr_message = {
|
|
26
|
+
"target": "agentEvent", # Client-side method name
|
|
27
|
+
"arguments": [event],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Send to SignalR
|
|
31
|
+
message_json = json.dumps(signalr_message)
|
|
32
|
+
self.signalr_out.set(message_json)
|
|
33
|
+
|
|
34
|
+
job_id = event.get("job_id", "unknown")
|
|
35
|
+
event_type = event.get("event_type", "unknown")
|
|
36
|
+
logger.debug(f"Published {event_type} to SignalR for job {job_id}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class JobType(Enum):
|
|
8
|
+
THREAD_MESSAGE = "thread_message"
|
|
9
|
+
ORCHESTRATION = "orchestration"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentJob(BaseModel):
|
|
13
|
+
id: str
|
|
14
|
+
job_type: JobType
|
|
15
|
+
|
|
16
|
+
def to_json(self) -> str:
|
|
17
|
+
"""Convert AgentJob to JSON string"""
|
|
18
|
+
return self.model_dump_json()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AgentJobQueue(ABC):
|
|
22
|
+
"""Minimal queue abstraction - just push jobs"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def push(self, job: AgentJob) -> None:
|
|
26
|
+
"""Push job to queue"""
|
|
27
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from spaik_sdk.server.queue.agent_job_queue import AgentJob
|
|
4
|
+
from spaik_sdk.utils.init_logger import init_logger
|
|
5
|
+
|
|
6
|
+
logger = init_logger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AzureFunctionQueue:
|
|
10
|
+
"""Azure Functions queue implementation"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, func_out: Any):
|
|
13
|
+
"""
|
|
14
|
+
Args:
|
|
15
|
+
func_out: Azure Functions queue output binding (func.Out[str])
|
|
16
|
+
"""
|
|
17
|
+
self.func_out = func_out
|
|
18
|
+
|
|
19
|
+
async def push(self, job: AgentJob) -> None:
|
|
20
|
+
"""Push job to Azure Functions queue"""
|
|
21
|
+
json_data = job.to_json()
|
|
22
|
+
|
|
23
|
+
# Set the message in the Azure Functions queue output
|
|
24
|
+
self.func_out.set(json_data)
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any, AsyncGenerator, Dict, Optional, TypeVar
|
|
4
|
+
|
|
5
|
+
from spaik_sdk.agent.base_agent import BaseAgent
|
|
6
|
+
from spaik_sdk.llm.cancellation_handle import CancellationHandle
|
|
7
|
+
from spaik_sdk.server.response.response_generator import ResponseGenerator
|
|
8
|
+
from spaik_sdk.thread.models import ThreadEvent
|
|
9
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
10
|
+
|
|
11
|
+
TAgent = TypeVar("TAgent", bound=BaseAgent)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentResponseGenerator(ResponseGenerator):
|
|
15
|
+
def __init__(self, agent: TAgent, call_agent: Callable[[TAgent], AsyncGenerator[ThreadEvent, None]]):
|
|
16
|
+
self.agent = agent
|
|
17
|
+
self.call_agent = call_agent
|
|
18
|
+
|
|
19
|
+
async def stream_response(
|
|
20
|
+
self, thread: ThreadContainer, cancellation_handle: Optional[CancellationHandle] = None
|
|
21
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
22
|
+
self.agent.set_thread_container(thread)
|
|
23
|
+
self.agent.set_cancellation_handle(cancellation_handle)
|
|
24
|
+
|
|
25
|
+
# Stream from the agent execution
|
|
26
|
+
async for event in self.call_agent(self.agent):
|
|
27
|
+
if not event.is_publishable():
|
|
28
|
+
continue
|
|
29
|
+
yield self._convert_event(event, thread.thread_id)
|
|
30
|
+
|
|
31
|
+
def _convert_event(self, event: ThreadEvent, thread_id: str) -> Dict[str, Any]:
|
|
32
|
+
"""Convert ThreadContainer event to publishable format"""
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"thread_id": thread_id,
|
|
36
|
+
"event_type": event.get_event_type(),
|
|
37
|
+
"timestamp": int(time.time() * 1000),
|
|
38
|
+
"data": event.get_event_data(),
|
|
39
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, AsyncGenerator, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.llm.cancellation_handle import CancellationHandle
|
|
5
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResponseGenerator(ABC):
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def stream_response(
|
|
11
|
+
self, thread: ThreadContainer, cancellation_handle: Optional[CancellationHandle] = None
|
|
12
|
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
|
13
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from spaik_sdk.agent.base_agent import BaseAgent
|
|
4
|
+
from spaik_sdk.server.response.agent_response_generator import AgentResponseGenerator
|
|
5
|
+
from spaik_sdk.thread.models import ThreadEvent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimpleAgentResponseGenerator(AgentResponseGenerator):
|
|
9
|
+
def __init__(self, agent: BaseAgent):
|
|
10
|
+
async def call_agent(agent: BaseAgent) -> AsyncGenerator[ThreadEvent, None]:
|
|
11
|
+
async for event in agent.get_event_stream():
|
|
12
|
+
yield event
|
|
13
|
+
|
|
14
|
+
super().__init__(agent, call_agent)
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.attachments.models import Attachment
|
|
5
|
+
from spaik_sdk.server.services.thread_models import (
|
|
6
|
+
AttachmentRequest,
|
|
7
|
+
AttachmentResponse,
|
|
8
|
+
MessageBlockRequest,
|
|
9
|
+
MessageBlockResponse,
|
|
10
|
+
MessageResponse,
|
|
11
|
+
ThreadMetadataResponse,
|
|
12
|
+
ThreadResponse,
|
|
13
|
+
)
|
|
14
|
+
from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
|
|
15
|
+
from spaik_sdk.thread.models import MessageBlock, ThreadMessage
|
|
16
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ThreadConverters:
|
|
20
|
+
"""Utility class for converting between internal and API models"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def block_request_to_model(request: MessageBlockRequest) -> MessageBlock:
|
|
24
|
+
"""Convert MessageBlockRequest to MessageBlock"""
|
|
25
|
+
return MessageBlock(
|
|
26
|
+
id=str(uuid.uuid4()),
|
|
27
|
+
streaming=False,
|
|
28
|
+
type=request.type,
|
|
29
|
+
content=request.content,
|
|
30
|
+
tool_call_id=request.tool_call_id,
|
|
31
|
+
tool_call_args=request.tool_call_args,
|
|
32
|
+
tool_name=request.tool_name,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def block_model_to_response(block: MessageBlock) -> MessageBlockResponse:
|
|
37
|
+
"""Convert MessageBlock to MessageBlockResponse"""
|
|
38
|
+
return MessageBlockResponse(
|
|
39
|
+
id=block.id,
|
|
40
|
+
streaming=block.streaming,
|
|
41
|
+
type=block.type,
|
|
42
|
+
content=block.content,
|
|
43
|
+
tool_call_id=block.tool_call_id,
|
|
44
|
+
tool_call_args=block.tool_call_args,
|
|
45
|
+
tool_name=block.tool_name,
|
|
46
|
+
tool_call_response=block.tool_call_response,
|
|
47
|
+
tool_call_error=block.tool_call_error,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def attachment_request_to_model(request: AttachmentRequest) -> Attachment:
|
|
52
|
+
"""Convert AttachmentRequest to Attachment"""
|
|
53
|
+
return Attachment(
|
|
54
|
+
file_id=request.file_id,
|
|
55
|
+
mime_type=request.mime_type,
|
|
56
|
+
filename=request.filename,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def attachment_model_to_response(attachment: Attachment) -> AttachmentResponse:
|
|
61
|
+
"""Convert Attachment to AttachmentResponse"""
|
|
62
|
+
return AttachmentResponse(
|
|
63
|
+
file_id=attachment.file_id,
|
|
64
|
+
mime_type=attachment.mime_type,
|
|
65
|
+
filename=attachment.filename,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def message_model_to_response(message: ThreadMessage) -> MessageResponse:
|
|
70
|
+
"""Convert ThreadMessage to MessageResponse"""
|
|
71
|
+
attachments = None
|
|
72
|
+
if message.attachments:
|
|
73
|
+
attachments = [ThreadConverters.attachment_model_to_response(att) for att in message.attachments]
|
|
74
|
+
return MessageResponse(
|
|
75
|
+
id=message.id,
|
|
76
|
+
ai=message.ai,
|
|
77
|
+
author_id=message.author_id,
|
|
78
|
+
author_name=message.author_name,
|
|
79
|
+
timestamp=message.timestamp,
|
|
80
|
+
blocks=[ThreadConverters.block_model_to_response(block) for block in message.blocks],
|
|
81
|
+
attachments=attachments,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def messages_model_to_response(messages: List[ThreadMessage]) -> List[MessageResponse]:
|
|
86
|
+
"""Convert list of ThreadMessage to list of MessageResponse"""
|
|
87
|
+
return [ThreadConverters.message_model_to_response(msg) for msg in messages]
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def thread_model_to_response(thread: ThreadContainer) -> ThreadResponse:
|
|
91
|
+
"""Convert ThreadContainer to ThreadResponse"""
|
|
92
|
+
return ThreadResponse(
|
|
93
|
+
id=thread.thread_id,
|
|
94
|
+
messages=[ThreadConverters.message_model_to_response(msg) for msg in thread.messages],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def metadata_model_to_response(metadata: ThreadMetadata) -> ThreadMetadataResponse:
|
|
99
|
+
"""Convert ThreadMetadata to ThreadMetadataResponse"""
|
|
100
|
+
return ThreadMetadataResponse(
|
|
101
|
+
thread_id=metadata.thread_id,
|
|
102
|
+
title=metadata.title,
|
|
103
|
+
message_count=metadata.message_count,
|
|
104
|
+
last_activity_time=metadata.last_activity_time,
|
|
105
|
+
created_at=metadata.created_at,
|
|
106
|
+
author_id=metadata.author_id,
|
|
107
|
+
type=metadata.type,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def metadata_list_to_response(metadata_list: List[ThreadMetadata]) -> List[ThreadMetadataResponse]:
|
|
112
|
+
"""Convert list of ThreadMetadata to list of ThreadMetadataResponse"""
|
|
113
|
+
return [ThreadConverters.metadata_model_to_response(metadata) for metadata in metadata_list]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from spaik_sdk.thread.models import MessageBlockType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MessageBlockRequest(BaseModel):
|
|
9
|
+
type: MessageBlockType
|
|
10
|
+
content: Optional[str] = None
|
|
11
|
+
tool_call_id: Optional[str] = None
|
|
12
|
+
tool_call_args: Optional[Dict[str, Any]] = None
|
|
13
|
+
tool_name: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MessageBlockResponse(BaseModel):
|
|
17
|
+
id: str
|
|
18
|
+
streaming: bool
|
|
19
|
+
type: MessageBlockType
|
|
20
|
+
content: Optional[str] = None
|
|
21
|
+
tool_call_id: Optional[str] = None
|
|
22
|
+
tool_call_args: Optional[Dict[str, Any]] = None
|
|
23
|
+
tool_name: Optional[str] = None
|
|
24
|
+
tool_call_response: Optional[str] = None
|
|
25
|
+
tool_call_error: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UpdateMessageRequest(BaseModel):
|
|
29
|
+
blocks: Optional[List[MessageBlockRequest]] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AttachmentRequest(BaseModel):
|
|
33
|
+
file_id: str
|
|
34
|
+
mime_type: str
|
|
35
|
+
filename: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AttachmentResponse(BaseModel):
|
|
39
|
+
file_id: str
|
|
40
|
+
mime_type: str
|
|
41
|
+
filename: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MessageResponse(BaseModel):
|
|
45
|
+
id: str
|
|
46
|
+
ai: bool
|
|
47
|
+
author_id: str
|
|
48
|
+
author_name: str
|
|
49
|
+
timestamp: int
|
|
50
|
+
blocks: List[MessageBlockResponse]
|
|
51
|
+
attachments: Optional[List[AttachmentResponse]] = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CreateMessageRequest(BaseModel):
|
|
55
|
+
content: str
|
|
56
|
+
attachments: Optional[List[AttachmentRequest]] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CreateThreadRequest(BaseModel):
|
|
60
|
+
job_id: Optional[str] = "unknown"
|
|
61
|
+
# TODO add metadata, remove job_id
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ThreadResponse(BaseModel):
|
|
65
|
+
id: str
|
|
66
|
+
messages: List[MessageResponse]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ThreadMetadataResponse(BaseModel):
|
|
70
|
+
thread_id: str
|
|
71
|
+
title: str
|
|
72
|
+
message_count: int
|
|
73
|
+
last_activity_time: int
|
|
74
|
+
created_at: int
|
|
75
|
+
author_id: str
|
|
76
|
+
type: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class ListThreadsRequest(BaseModel):
|
|
80
|
+
author_id: Optional[str] = None
|
|
81
|
+
thread_type: Optional[str] = None
|
|
82
|
+
title_contains: Optional[str] = None
|
|
83
|
+
min_messages: Optional[int] = None
|
|
84
|
+
max_messages: Optional[int] = None
|
|
85
|
+
hours_ago: Optional[int] = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ListThreadsResponse(BaseModel):
|
|
89
|
+
threads: List[ThreadMetadataResponse]
|
|
90
|
+
total_count: int
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from spaik_sdk.server.storage.base_thread_repository import BaseThreadRepository
|
|
4
|
+
from spaik_sdk.server.storage.thread_filter import ThreadFilter
|
|
5
|
+
from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
|
|
6
|
+
from spaik_sdk.thread.models import ThreadMessage
|
|
7
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ThreadService:
|
|
11
|
+
"""High-level service for thread and message operations"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, repository: BaseThreadRepository):
|
|
14
|
+
self.repository = repository
|
|
15
|
+
|
|
16
|
+
# Thread operations
|
|
17
|
+
async def create_thread(self, thread_container: ThreadContainer) -> ThreadContainer:
|
|
18
|
+
"""Create a new thread"""
|
|
19
|
+
await self.repository.save_thread(thread_container)
|
|
20
|
+
return thread_container
|
|
21
|
+
|
|
22
|
+
async def get_thread(self, thread_id: str) -> Optional[ThreadContainer]:
|
|
23
|
+
"""Get thread by ID"""
|
|
24
|
+
return await self.repository.load_thread(thread_id)
|
|
25
|
+
|
|
26
|
+
async def update_thread(self, thread_container: ThreadContainer) -> Optional[ThreadContainer]:
|
|
27
|
+
"""Update an existing thread"""
|
|
28
|
+
if not await self.repository.thread_exists(thread_container.thread_id):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
await self.repository.save_thread(thread_container)
|
|
32
|
+
return thread_container
|
|
33
|
+
|
|
34
|
+
async def delete_thread(self, thread_id: str) -> bool:
|
|
35
|
+
"""Delete thread and all its messages"""
|
|
36
|
+
return await self.repository.delete_thread(thread_id)
|
|
37
|
+
|
|
38
|
+
async def thread_exists(self, thread_id: str) -> bool:
|
|
39
|
+
"""Check if thread exists"""
|
|
40
|
+
return await self.repository.thread_exists(thread_id)
|
|
41
|
+
|
|
42
|
+
async def list_threads(self, filter: ThreadFilter) -> List[ThreadMetadata]:
|
|
43
|
+
"""List threads matching filter"""
|
|
44
|
+
return await self.repository.list_threads(filter)
|
|
45
|
+
|
|
46
|
+
# Message operations
|
|
47
|
+
async def get_message(self, thread_id: str, message_id: str) -> Optional[ThreadMessage]:
|
|
48
|
+
"""Get message by thread ID and message ID"""
|
|
49
|
+
return await self.repository.get_message(thread_id, message_id)
|
|
50
|
+
|
|
51
|
+
async def add_message(self, thread_id: str, message: ThreadMessage) -> Optional[ThreadMessage]:
|
|
52
|
+
"""Add message to thread"""
|
|
53
|
+
if not await self.repository.thread_exists(thread_id):
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
await self.repository.upsert_message(thread_id, message)
|
|
57
|
+
return message
|
|
58
|
+
|
|
59
|
+
async def update_message(self, thread_id: str, message: ThreadMessage) -> Optional[ThreadMessage]:
|
|
60
|
+
"""Update existing message in thread"""
|
|
61
|
+
if not await self.repository.thread_exists(thread_id):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
# Check if message exists
|
|
65
|
+
existing_message = await self.repository.get_message(thread_id, message.id)
|
|
66
|
+
if not existing_message:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
await self.repository.upsert_message(thread_id, message)
|
|
70
|
+
return message
|
|
71
|
+
|
|
72
|
+
async def delete_message(self, thread_id: str, message_id: str) -> bool:
|
|
73
|
+
"""Delete message from thread"""
|
|
74
|
+
if not await self.repository.thread_exists(thread_id):
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# Check if message exists
|
|
78
|
+
existing_message = await self.repository.get_message(thread_id, message_id)
|
|
79
|
+
if not existing_message:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
await self.repository.delete_message(thread_id, message_id)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
async def get_thread_messages(self, thread_id: str) -> List[ThreadMessage]:
|
|
86
|
+
"""Get all messages for a thread"""
|
|
87
|
+
thread = await self.repository.load_thread(thread_id)
|
|
88
|
+
if not thread:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
return thread.messages
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.server.storage.thread_filter import ThreadFilter
|
|
5
|
+
from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
|
|
6
|
+
from spaik_sdk.thread.models import ThreadMessage
|
|
7
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseThreadRepository(ABC):
|
|
11
|
+
"""Abstract base class for thread persistence"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def save_thread(self, thread_container: ThreadContainer) -> None:
|
|
15
|
+
"""Save complete thread container to storage"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
async def load_thread(self, thread_id: str) -> Optional[ThreadContainer]:
|
|
20
|
+
"""Load thread container from storage"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def get_message(self, thread_id: str, message_id: str) -> Optional[ThreadMessage]:
|
|
25
|
+
"""Get message from storage"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def upsert_message(self, thread_id: str, message: ThreadMessage) -> None:
|
|
30
|
+
"""Upsert message to storage"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def delete_message(self, thread_id: str, message_id: str) -> None:
|
|
35
|
+
"""Delete message from storage"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
async def thread_exists(self, thread_id: str) -> bool:
|
|
40
|
+
"""Check if thread exists"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def delete_thread(self, thread_id: str) -> bool:
|
|
45
|
+
"""Delete thread and all its messages"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
async def list_threads(self, filter: ThreadFilter) -> List[ThreadMetadata]:
|
|
50
|
+
"""List threads for a given filter"""
|
|
51
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from spaik_sdk.server.storage.base_thread_repository import BaseThreadRepository
|
|
4
|
+
from spaik_sdk.server.storage.thread_filter import ThreadFilter
|
|
5
|
+
from spaik_sdk.server.storage.thread_metadata import ThreadMetadata
|
|
6
|
+
from spaik_sdk.thread.models import ThreadMessage
|
|
7
|
+
from spaik_sdk.thread.thread_container import ThreadContainer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InMemoryThreadRepository(BaseThreadRepository):
|
|
11
|
+
"""In-memory implementation of thread repository for testing/development"""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._threads: Dict[str, ThreadContainer] = {}
|
|
15
|
+
self._metadata: Dict[str, ThreadMetadata] = {}
|
|
16
|
+
|
|
17
|
+
async def save_thread(self, thread_container: ThreadContainer) -> None:
|
|
18
|
+
"""Save complete thread container to memory"""
|
|
19
|
+
self._threads[thread_container.thread_id] = thread_container
|
|
20
|
+
# Update metadata
|
|
21
|
+
metadata = ThreadMetadata.from_thread_container(thread_container)
|
|
22
|
+
self._metadata[thread_container.thread_id] = metadata
|
|
23
|
+
|
|
24
|
+
async def load_thread(self, thread_id: str) -> Optional[ThreadContainer]:
|
|
25
|
+
"""Load thread container from memory"""
|
|
26
|
+
return self._threads.get(thread_id)
|
|
27
|
+
|
|
28
|
+
async def get_message(self, thread_id: str, message_id: str) -> Optional[ThreadMessage]:
|
|
29
|
+
"""Get message from memory"""
|
|
30
|
+
thread = self._threads.get(thread_id)
|
|
31
|
+
if not thread:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
for message in thread.messages:
|
|
35
|
+
if message.id == message_id:
|
|
36
|
+
return message
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
async def upsert_message(self, thread_id: str, message: ThreadMessage) -> None:
|
|
40
|
+
"""Upsert message to memory"""
|
|
41
|
+
thread = self._threads.get(thread_id)
|
|
42
|
+
if not thread:
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
# Find existing message and replace, or add new one
|
|
46
|
+
for i, existing_msg in enumerate(thread.messages):
|
|
47
|
+
if existing_msg.id == message.id:
|
|
48
|
+
thread.messages[i] = message
|
|
49
|
+
break
|
|
50
|
+
else:
|
|
51
|
+
thread.messages.append(message)
|
|
52
|
+
|
|
53
|
+
# Update metadata
|
|
54
|
+
metadata = ThreadMetadata.from_thread_container(thread)
|
|
55
|
+
self._metadata[thread_id] = metadata
|
|
56
|
+
|
|
57
|
+
async def delete_message(self, thread_id: str, message_id: str) -> None:
|
|
58
|
+
"""Delete message from memory"""
|
|
59
|
+
thread = self._threads.get(thread_id)
|
|
60
|
+
if not thread:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
thread.messages = [msg for msg in thread.messages if msg.id != message_id]
|
|
64
|
+
|
|
65
|
+
# Update metadata
|
|
66
|
+
metadata = ThreadMetadata.from_thread_container(thread)
|
|
67
|
+
self._metadata[thread_id] = metadata
|
|
68
|
+
|
|
69
|
+
async def thread_exists(self, thread_id: str) -> bool:
|
|
70
|
+
"""Check if thread exists in memory"""
|
|
71
|
+
return thread_id in self._threads
|
|
72
|
+
|
|
73
|
+
async def delete_thread(self, thread_id: str) -> bool:
|
|
74
|
+
"""Delete thread and all its messages from memory"""
|
|
75
|
+
if thread_id in self._threads:
|
|
76
|
+
del self._threads[thread_id]
|
|
77
|
+
if thread_id in self._metadata:
|
|
78
|
+
del self._metadata[thread_id]
|
|
79
|
+
return True
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
async def list_threads(self, filter: ThreadFilter) -> List[ThreadMetadata]:
|
|
83
|
+
"""List threads matching the filter from memory"""
|
|
84
|
+
result = []
|
|
85
|
+
for metadata in self._metadata.values():
|
|
86
|
+
if filter.matches(metadata):
|
|
87
|
+
result.append(metadata)
|
|
88
|
+
|
|
89
|
+
# Sort by last activity time (most recent first)
|
|
90
|
+
result.sort(key=lambda x: x.last_activity_time, reverse=True)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
def clear_all(self) -> None:
|
|
94
|
+
"""Clear all data (useful for testing)"""
|
|
95
|
+
self._threads.clear()
|
|
96
|
+
self._metadata.clear()
|
|
97
|
+
|
|
98
|
+
def get_thread_count(self) -> int:
|
|
99
|
+
"""Get total number of threads stored"""
|
|
100
|
+
return len(self._threads)
|