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,131 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
3
|
+
|
|
4
|
+
from langchain_core.tools import BaseTool, StructuredTool, tool
|
|
5
|
+
from pydantic import BaseModel, Field, create_model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolProvider(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract class for tool providers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def get_tools(self) -> List[BaseTool]:
|
|
15
|
+
"""
|
|
16
|
+
Returns a Langchain BaseTool implementation.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
BaseTool: A Langchain tool
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def create_tool(
|
|
25
|
+
func: Callable[..., Any], name: str, description: str, args_schema: Optional[type[BaseModel]] = None, **kwargs
|
|
26
|
+
) -> BaseTool:
|
|
27
|
+
"""
|
|
28
|
+
Create a tool with customizable name and description.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
func: The function to wrap as a tool
|
|
32
|
+
name: The tool name
|
|
33
|
+
description: The tool description (overrides docstring)
|
|
34
|
+
args_schema: Optional Pydantic model for argument validation
|
|
35
|
+
**kwargs: Additional arguments passed to StructuredTool.from_function
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
BaseTool: A configured langchain tool
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
def my_func(text: str) -> str:
|
|
42
|
+
return f"Processed: {text}"
|
|
43
|
+
|
|
44
|
+
# Basic usage
|
|
45
|
+
tool = create_tool(my_func, "process", "Process text input")
|
|
46
|
+
|
|
47
|
+
# With custom schema
|
|
48
|
+
class MyArgs(BaseModel):
|
|
49
|
+
text: str = Field(description="Text to process")
|
|
50
|
+
|
|
51
|
+
tool = create_tool(my_func, "process", "Process text", MyArgs)
|
|
52
|
+
"""
|
|
53
|
+
return StructuredTool.from_function(func=func, name=name, description=description, args_schema=args_schema, **kwargs)
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def create_dynamic_tool(
|
|
57
|
+
func: Callable[..., Any], name: str, description: str, args: Optional[Dict[str, Union[type, Tuple[type, str]]]] = None, **kwargs
|
|
58
|
+
) -> BaseTool:
|
|
59
|
+
"""
|
|
60
|
+
Create a tool with dynamic argument schema.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
func: The function to wrap as a tool
|
|
64
|
+
name: The tool name
|
|
65
|
+
description: The tool description
|
|
66
|
+
args: Dict mapping arg names to types or (type, description) tuples
|
|
67
|
+
**kwargs: Additional arguments passed to StructuredTool.from_function
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
BaseTool: A configured langchain tool
|
|
71
|
+
|
|
72
|
+
Example:
|
|
73
|
+
def search(query: str, limit: int = 10) -> str:
|
|
74
|
+
return f"Found results for {query}"
|
|
75
|
+
|
|
76
|
+
tool = create_dynamic_tool(
|
|
77
|
+
search,
|
|
78
|
+
"search_tool",
|
|
79
|
+
"Search for information",
|
|
80
|
+
args={
|
|
81
|
+
"query": (str, "The search query"),
|
|
82
|
+
"limit": (int, "Max results (default: 10)")
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
"""
|
|
86
|
+
args_schema = None
|
|
87
|
+
if args:
|
|
88
|
+
# Build field definitions for dynamic schema
|
|
89
|
+
fields = {}
|
|
90
|
+
for arg_name, arg_spec in args.items():
|
|
91
|
+
if isinstance(arg_spec, tuple):
|
|
92
|
+
arg_type, arg_desc = arg_spec
|
|
93
|
+
fields[arg_name] = (arg_type, Field(description=arg_desc))
|
|
94
|
+
else:
|
|
95
|
+
fields[arg_name] = (arg_spec, Field())
|
|
96
|
+
|
|
97
|
+
# Create dynamic Pydantic model
|
|
98
|
+
args_schema = create_model(f"{name}Args", **fields) # type: ignore
|
|
99
|
+
|
|
100
|
+
return StructuredTool.from_function(func=func, name=name, description=description, args_schema=args_schema, **kwargs)
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def create_simple_tool(func: Callable[..., Any], name: str, description: str, **kwargs) -> BaseTool:
|
|
104
|
+
"""
|
|
105
|
+
Create a simple tool without argument validation.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
func: The function to wrap as a tool
|
|
109
|
+
name: The tool name
|
|
110
|
+
description: The tool description
|
|
111
|
+
**kwargs: Additional arguments passed to StructuredTool.from_function
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
BaseTool: A configured langchain tool
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
def get_time() -> str:
|
|
118
|
+
import datetime
|
|
119
|
+
return datetime.datetime.now().isoformat()
|
|
120
|
+
|
|
121
|
+
tool = create_simple_tool(
|
|
122
|
+
get_time,
|
|
123
|
+
"current_time",
|
|
124
|
+
"Get current timestamp"
|
|
125
|
+
)
|
|
126
|
+
"""
|
|
127
|
+
return StructuredTool.from_function(func=func, name=name, description=description, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Re-export langchain tools for convenience
|
|
131
|
+
__all__ = ["ToolProvider", "BaseTool", "tool"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from spaik_sdk.tracing.agent_trace import AgentTrace
|
|
2
|
+
from spaik_sdk.tracing.get_trace_sink import get_trace_sink
|
|
3
|
+
from spaik_sdk.tracing.local_trace_sink import LocalTraceSink
|
|
4
|
+
from spaik_sdk.tracing.trace_sink import TraceSink
|
|
5
|
+
from spaik_sdk.tracing.trace_sink_mode import TraceSinkMode
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentTrace",
|
|
9
|
+
"TraceSink",
|
|
10
|
+
"LocalTraceSink",
|
|
11
|
+
"TraceSinkMode",
|
|
12
|
+
"get_trace_sink",
|
|
13
|
+
]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional, Type
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from spaik_sdk.thread.models import MessageBlock, MessageBlockType
|
|
8
|
+
from spaik_sdk.tracing.get_trace_sink import get_trace_sink
|
|
9
|
+
from spaik_sdk.tracing.trace_sink import TraceSink
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentTrace:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
system_prompt: str,
|
|
16
|
+
save_name: Optional[str] = None,
|
|
17
|
+
trace_sink: Optional[TraceSink] = None,
|
|
18
|
+
):
|
|
19
|
+
self.system_prompt: str = system_prompt
|
|
20
|
+
self._start_time_monotonic: float = time.monotonic()
|
|
21
|
+
self._steps: list[tuple[float, str]] = []
|
|
22
|
+
self.save_name: Optional[str] = save_name
|
|
23
|
+
self._trace_sink: TraceSink = trace_sink or get_trace_sink()
|
|
24
|
+
|
|
25
|
+
def add_step(self, step_content: str) -> None:
|
|
26
|
+
current_time_monotonic: float = time.monotonic()
|
|
27
|
+
elapsed_time: float = current_time_monotonic - self._start_time_monotonic
|
|
28
|
+
self._steps.append((elapsed_time, step_content))
|
|
29
|
+
if self.save_name is not None:
|
|
30
|
+
self.save(self.save_name)
|
|
31
|
+
|
|
32
|
+
def add_structured_response_input(self, input: str, model: Type[BaseModel]) -> None:
|
|
33
|
+
self.add_step(f"📄: {input} \n {json.dumps(model.model_json_schema(), indent=2)}")
|
|
34
|
+
|
|
35
|
+
def add_structured_response_output(self, output: BaseModel) -> None:
|
|
36
|
+
self.add_step(f"```json\n{json.dumps(output.model_dump(), indent=2)}\n```")
|
|
37
|
+
|
|
38
|
+
def add_input(self, input: Optional[str] = None) -> None:
|
|
39
|
+
if input is None:
|
|
40
|
+
return
|
|
41
|
+
self.add_step(f"👤: {input}")
|
|
42
|
+
|
|
43
|
+
def add_block(self, block: MessageBlock) -> None:
|
|
44
|
+
if block.type == MessageBlockType.PLAIN:
|
|
45
|
+
self.add_step(f"🤖: {block.content}")
|
|
46
|
+
elif block.type == MessageBlockType.REASONING:
|
|
47
|
+
self.add_step(f"🧠: {block.content}")
|
|
48
|
+
elif block.type == MessageBlockType.TOOL_USE:
|
|
49
|
+
self.add_step(f"🔧: {block.tool_name} {json.dumps(block.tool_call_args, indent=2)}")
|
|
50
|
+
elif block.type == MessageBlockType.ERROR:
|
|
51
|
+
self.add_step(f"🚨: {block.content}")
|
|
52
|
+
else:
|
|
53
|
+
self.add_step(f"❓: {block.content}")
|
|
54
|
+
|
|
55
|
+
def to_string(self, include_system_prompt: bool = True) -> str:
|
|
56
|
+
lines: list[str] = []
|
|
57
|
+
if include_system_prompt:
|
|
58
|
+
lines.append("[system prompt]")
|
|
59
|
+
lines.append("")
|
|
60
|
+
lines.append(self.system_prompt)
|
|
61
|
+
|
|
62
|
+
for elapsed_seconds, content in self._steps:
|
|
63
|
+
lines.append("")
|
|
64
|
+
lines.append(f"[{elapsed_seconds:.1f}s]")
|
|
65
|
+
lines.append("")
|
|
66
|
+
lines.append(content)
|
|
67
|
+
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
def save(self, name: str) -> None:
|
|
71
|
+
trace_content = self.to_string(include_system_prompt=False)
|
|
72
|
+
self._trace_sink.save_trace(name, trace_content, self.system_prompt)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from spaik_sdk.tracing.local_trace_sink import LocalTraceSink
|
|
4
|
+
from spaik_sdk.tracing.trace_sink import TraceSink
|
|
5
|
+
from spaik_sdk.tracing.trace_sink_mode import TraceSinkMode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_trace_sink(mode: Optional[TraceSinkMode] = None) -> TraceSink:
|
|
9
|
+
# Lazy import to avoid circular dependency with env.py
|
|
10
|
+
from spaik_sdk.config.env import env_config
|
|
11
|
+
|
|
12
|
+
mode = mode or env_config.get_trace_sink_mode()
|
|
13
|
+
if mode == TraceSinkMode.LOCAL:
|
|
14
|
+
return LocalTraceSink()
|
|
15
|
+
raise ValueError(f"Unknown TraceSinkMode: {mode}")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from spaik_sdk.tracing.trace_sink import TraceSink
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LocalTraceSink(TraceSink):
|
|
8
|
+
"""TraceSink implementation that writes traces to the local filesystem."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, traces_dir: Optional[str] = None):
|
|
11
|
+
self.traces_dir = traces_dir or "traces"
|
|
12
|
+
|
|
13
|
+
def save_trace(self, name: str, trace_content: str, system_prompt: str) -> None:
|
|
14
|
+
os.makedirs(self.traces_dir, exist_ok=True)
|
|
15
|
+
|
|
16
|
+
trace_path = os.path.join(self.traces_dir, f"{name}.txt")
|
|
17
|
+
system_prompt_path = os.path.join(self.traces_dir, f"{name}_system_prompt.txt")
|
|
18
|
+
|
|
19
|
+
with open(trace_path, "w", encoding="utf-8") as f:
|
|
20
|
+
f.write(trace_content)
|
|
21
|
+
|
|
22
|
+
with open(system_prompt_path, "w", encoding="utf-8") as f:
|
|
23
|
+
f.write(system_prompt)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TraceSink(ABC):
|
|
5
|
+
"""Abstract base class for trace storage backends.
|
|
6
|
+
|
|
7
|
+
Implementations can write traces to local files, remote APIs, databases, etc.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def save_trace(self, name: str, trace_content: str, system_prompt: str) -> None:
|
|
12
|
+
"""Save a trace with its system prompt.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
name: Identifier for the trace (e.g., agent class name)
|
|
16
|
+
trace_content: The formatted trace content (without system prompt)
|
|
17
|
+
system_prompt: The system prompt used for the agent
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TraceSinkMode(Enum):
|
|
5
|
+
LOCAL = "local"
|
|
6
|
+
|
|
7
|
+
@classmethod
|
|
8
|
+
def from_name(cls, name: str) -> "TraceSinkMode":
|
|
9
|
+
for mode in cls:
|
|
10
|
+
if mode.value == name:
|
|
11
|
+
return mode
|
|
12
|
+
|
|
13
|
+
available_modes = [mode.value for mode in cls]
|
|
14
|
+
raise ValueError(f"Unknown TraceSinkMode '{name}'. Available: {', '.join(available_modes)}")
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
|
|
6
|
+
TRACE_LEVEL = 5
|
|
7
|
+
logging.addLevelName(TRACE_LEVEL, "TRACE")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CustomLogger(logging.Logger):
|
|
11
|
+
def trace(self, msg, *args, **kwargs):
|
|
12
|
+
if self.isEnabledFor(TRACE_LEVEL):
|
|
13
|
+
self._log(TRACE_LEVEL, msg, args, **kwargs)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def init_logger(name: str) -> CustomLogger:
|
|
17
|
+
logging.setLoggerClass(CustomLogger)
|
|
18
|
+
logging.basicConfig(
|
|
19
|
+
level=getattr(logging, LOG_LEVEL, logging.INFO if LOG_LEVEL == "INFO" else logging.DEBUG),
|
|
20
|
+
format="%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s",
|
|
21
|
+
force=True, # This ensures we override any existing config
|
|
22
|
+
)
|
|
23
|
+
logger = logging.getLogger(name)
|
|
24
|
+
return cast(CustomLogger, logger)
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spaik-sdk
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Summary: Python SDK for building AI agents with multi-LLM support, streaming, and production-ready infrastructure
|
|
5
|
+
Project-URL: Homepage, https://github.com/siilisolutions/spaik-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/siilisolutions/spaik-sdk
|
|
7
|
+
Project-URL: Documentation, https://github.com/siilisolutions/spaik-sdk#readme
|
|
8
|
+
Author-email: Siili Solutions Oyj <info@siili.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,anthropic,claude,gpt,langchain,llm,openai,streaming
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: aioconsole>=0.8.1
|
|
21
|
+
Requires-Dist: azure-storage-blob
|
|
22
|
+
Requires-Dist: cryptography>=41.0.0
|
|
23
|
+
Requires-Dist: dotenv>=0.9.9
|
|
24
|
+
Requires-Dist: fastapi>=0.115.12
|
|
25
|
+
Requires-Dist: httpx>=0.25.0
|
|
26
|
+
Requires-Dist: langchain-anthropic>=1.3.0
|
|
27
|
+
Requires-Dist: langchain-core>=1.2.0
|
|
28
|
+
Requires-Dist: langchain-google-genai>=4.0.0
|
|
29
|
+
Requires-Dist: langchain-mcp-adapters>=0.2.1
|
|
30
|
+
Requires-Dist: langchain-ollama>=0.3.0
|
|
31
|
+
Requires-Dist: langchain-openai>=1.1.0
|
|
32
|
+
Requires-Dist: langchain-tavily>=0.2.15
|
|
33
|
+
Requires-Dist: langchain>=1.2.0
|
|
34
|
+
Requires-Dist: langgraph>=1.0.0
|
|
35
|
+
Requires-Dist: mcp>=1.9.2
|
|
36
|
+
Requires-Dist: pandas-stubs
|
|
37
|
+
Requires-Dist: pandas>=2.0.3
|
|
38
|
+
Requires-Dist: pyjwt>=2.8.0
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.21.1
|
|
40
|
+
Requires-Dist: pytest-cov>=4.1.0
|
|
41
|
+
Requires-Dist: pytest-mock>=3.11.1
|
|
42
|
+
Requires-Dist: pytest>=7.4.0
|
|
43
|
+
Requires-Dist: requests>=2.31.0
|
|
44
|
+
Requires-Dist: restrictedpython>=8.0
|
|
45
|
+
Requires-Dist: rich>=14.0.0
|
|
46
|
+
Requires-Dist: uvicorn>=0.33.0
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: black; extra == 'dev'
|
|
49
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
50
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
51
|
+
Description-Content-Type: text/markdown
|
|
52
|
+
|
|
53
|
+
# Spaik SDK
|
|
54
|
+
|
|
55
|
+
Python SDK for building AI agents with multi-LLM support, streaming, and production infrastructure.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install spaik-sdk
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from spaik_sdk.agent.base_agent import BaseAgent
|
|
67
|
+
|
|
68
|
+
class MyAgent(BaseAgent):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
agent = MyAgent(system_prompt="You are a helpful assistant.")
|
|
72
|
+
print(agent.get_response_text("Hello!"))
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
- **Multi-LLM Support**: OpenAI, Anthropic, Google, Azure, Ollama
|
|
78
|
+
- **Unified API**: Same interface across all providers
|
|
79
|
+
- **Streaming**: Real-time response streaming via SSE
|
|
80
|
+
- **Tools**: Function calling with LangChain integration
|
|
81
|
+
- **Structured Output**: Pydantic model responses
|
|
82
|
+
- **Server**: FastAPI with thread persistence, auth, file uploads
|
|
83
|
+
- **Audio**: Text-to-speech and speech-to-text
|
|
84
|
+
- **Cost Tracking**: Token usage and cost estimation
|
|
85
|
+
|
|
86
|
+
## Agent API
|
|
87
|
+
|
|
88
|
+
### Basic Response Methods
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from spaik_sdk.agent.base_agent import BaseAgent
|
|
92
|
+
from spaik_sdk.models.model_registry import ModelRegistry
|
|
93
|
+
|
|
94
|
+
agent = MyAgent(
|
|
95
|
+
system_prompt="You are helpful.",
|
|
96
|
+
llm_model=ModelRegistry.CLAUDE_4_SONNET
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Sync - text only
|
|
100
|
+
text = agent.get_response_text("Hello")
|
|
101
|
+
|
|
102
|
+
# Sync - full message with blocks
|
|
103
|
+
message = agent.get_response("Hello")
|
|
104
|
+
print(message.get_text_content())
|
|
105
|
+
|
|
106
|
+
# Async
|
|
107
|
+
message = await agent.get_response_async("Hello")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Streaming
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
# Token stream
|
|
114
|
+
async for chunk in agent.get_response_stream("Write a story"):
|
|
115
|
+
print(chunk, end="", flush=True)
|
|
116
|
+
|
|
117
|
+
# Event stream (for SSE)
|
|
118
|
+
async for event in agent.get_event_stream("Write a story"):
|
|
119
|
+
if event.get_event_type() == "StreamingUpdated":
|
|
120
|
+
print(event.content, end="")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Structured Output
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from pydantic import BaseModel
|
|
127
|
+
|
|
128
|
+
class Recipe(BaseModel):
|
|
129
|
+
name: str
|
|
130
|
+
ingredients: list[str]
|
|
131
|
+
steps: list[str]
|
|
132
|
+
|
|
133
|
+
recipe = agent.get_structured_response("Give me a pasta recipe", Recipe)
|
|
134
|
+
print(recipe.name)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Interactive CLI
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
agent.run_cli() # Starts interactive chat in terminal
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Tools
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from spaik_sdk.tools.tool_provider import ToolProvider, BaseTool, tool
|
|
147
|
+
|
|
148
|
+
class WeatherTools(ToolProvider):
|
|
149
|
+
def get_tools(self) -> list[BaseTool]:
|
|
150
|
+
@tool
|
|
151
|
+
def get_weather(city: str) -> str:
|
|
152
|
+
"""Get current weather for a city."""
|
|
153
|
+
return f"Sunny, 22°C in {city}"
|
|
154
|
+
|
|
155
|
+
@tool
|
|
156
|
+
def get_forecast(city: str, days: int = 3) -> str:
|
|
157
|
+
"""Get weather forecast."""
|
|
158
|
+
return f"{days}-day forecast for {city}: Sunny"
|
|
159
|
+
|
|
160
|
+
return [get_weather, get_forecast]
|
|
161
|
+
|
|
162
|
+
class WeatherAgent(BaseAgent):
|
|
163
|
+
def get_tool_providers(self) -> list[ToolProvider]:
|
|
164
|
+
return [WeatherTools()]
|
|
165
|
+
|
|
166
|
+
agent = WeatherAgent(system_prompt="You provide weather info.")
|
|
167
|
+
print(agent.get_response_text("What's the weather in Tokyo?"))
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Built-in Tool Providers
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from spaik_sdk.tools.impl.search_tool_provider import SearchToolProvider
|
|
174
|
+
from spaik_sdk.tools.impl.mcp_tool_provider import MCPToolProvider
|
|
175
|
+
|
|
176
|
+
class MyAgent(BaseAgent):
|
|
177
|
+
def get_tool_providers(self):
|
|
178
|
+
return [
|
|
179
|
+
SearchToolProvider(), # Web search (Tavily)
|
|
180
|
+
MCPToolProvider(server), # MCP server tools
|
|
181
|
+
]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Models
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from spaik_sdk.models.model_registry import ModelRegistry
|
|
188
|
+
|
|
189
|
+
# Anthropic
|
|
190
|
+
ModelRegistry.CLAUDE_4_SONNET
|
|
191
|
+
ModelRegistry.CLAUDE_4_OPUS
|
|
192
|
+
ModelRegistry.CLAUDE_4_5_SONNET
|
|
193
|
+
ModelRegistry.CLAUDE_4_5_OPUS
|
|
194
|
+
|
|
195
|
+
# OpenAI
|
|
196
|
+
ModelRegistry.GPT_4_1
|
|
197
|
+
ModelRegistry.GPT_4O
|
|
198
|
+
ModelRegistry.O4_MINI
|
|
199
|
+
|
|
200
|
+
# Google
|
|
201
|
+
ModelRegistry.GEMINI_2_5_FLASH
|
|
202
|
+
ModelRegistry.GEMINI_2_5_PRO
|
|
203
|
+
|
|
204
|
+
# Aliases
|
|
205
|
+
ModelRegistry.from_name("sonnet") # CLAUDE_4_SONNET
|
|
206
|
+
ModelRegistry.from_name("gpt 4.1") # GPT_4_1
|
|
207
|
+
ModelRegistry.from_name("gemini 2.5") # GEMINI_2_5_FLASH
|
|
208
|
+
|
|
209
|
+
# Custom model
|
|
210
|
+
from spaik_sdk.models.llm_model import LLMModel
|
|
211
|
+
from spaik_sdk.models.llm_families import LLMFamilies
|
|
212
|
+
|
|
213
|
+
custom = LLMModel(
|
|
214
|
+
family=LLMFamilies.OPENAI,
|
|
215
|
+
name="gpt-4-custom",
|
|
216
|
+
reasoning=False
|
|
217
|
+
)
|
|
218
|
+
ModelRegistry.register_custom(custom)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## FastAPI Server
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from contextlib import asynccontextmanager
|
|
225
|
+
from fastapi import FastAPI
|
|
226
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
227
|
+
from spaik_sdk.agent.base_agent import BaseAgent
|
|
228
|
+
from spaik_sdk.server.api.routers.api_builder import ApiBuilder
|
|
229
|
+
|
|
230
|
+
class MyAgent(BaseAgent):
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
@asynccontextmanager
|
|
234
|
+
async def lifespan(app: FastAPI):
|
|
235
|
+
agent = MyAgent(system_prompt="You are helpful.")
|
|
236
|
+
api_builder = ApiBuilder.local(agent=agent)
|
|
237
|
+
|
|
238
|
+
app.include_router(api_builder.build_thread_router())
|
|
239
|
+
app.include_router(api_builder.build_file_router())
|
|
240
|
+
app.include_router(api_builder.build_audio_router())
|
|
241
|
+
yield
|
|
242
|
+
|
|
243
|
+
app = FastAPI(lifespan=lifespan)
|
|
244
|
+
app.add_middleware(
|
|
245
|
+
CORSMiddleware,
|
|
246
|
+
allow_origins=["*"],
|
|
247
|
+
allow_methods=["*"],
|
|
248
|
+
allow_headers=["*"],
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### API Endpoints
|
|
253
|
+
|
|
254
|
+
Thread management:
|
|
255
|
+
- `POST /threads` - Create thread
|
|
256
|
+
- `GET /threads` - List threads
|
|
257
|
+
- `GET /threads/{id}` - Get thread with messages
|
|
258
|
+
- `POST /threads/{id}/messages/stream` - Send message (SSE)
|
|
259
|
+
- `DELETE /threads/{id}` - Delete thread
|
|
260
|
+
- `POST /threads/{id}/cancel` - Cancel generation
|
|
261
|
+
|
|
262
|
+
Files:
|
|
263
|
+
- `POST /files` - Upload file
|
|
264
|
+
- `GET /files/{id}` - Download file
|
|
265
|
+
|
|
266
|
+
Audio:
|
|
267
|
+
- `POST /audio/speech` - Text to speech
|
|
268
|
+
- `POST /audio/transcribe` - Speech to text
|
|
269
|
+
|
|
270
|
+
### Production Setup
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from spaik_sdk.server.storage.impl.local_file_thread_repository import LocalFileThreadRepository
|
|
274
|
+
from spaik_sdk.server.authorization.base_authorizer import BaseAuthorizer
|
|
275
|
+
|
|
276
|
+
# Custom repository and auth
|
|
277
|
+
api_builder = ApiBuilder.stateful(
|
|
278
|
+
repository=LocalFileThreadRepository(base_path="./data"),
|
|
279
|
+
authorizer=MyAuthorizer(),
|
|
280
|
+
agent=agent,
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Orchestration
|
|
285
|
+
|
|
286
|
+
Code-first workflow orchestration without graph DSLs:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from spaik_sdk.orchestration import BaseOrchestrator, OrchestratorEvent
|
|
290
|
+
from dataclasses import dataclass
|
|
291
|
+
from typing import AsyncIterator
|
|
292
|
+
|
|
293
|
+
@dataclass
|
|
294
|
+
class State:
|
|
295
|
+
items: list[str]
|
|
296
|
+
|
|
297
|
+
@dataclass
|
|
298
|
+
class Result:
|
|
299
|
+
count: int
|
|
300
|
+
|
|
301
|
+
class MyOrchestrator(BaseOrchestrator[State, Result]):
|
|
302
|
+
async def run(self) -> AsyncIterator[OrchestratorEvent[Result]]:
|
|
303
|
+
state = State(items=[])
|
|
304
|
+
|
|
305
|
+
# Run step with automatic status events
|
|
306
|
+
async for event in self.step("fetch", "Fetching data", self.fetch, state):
|
|
307
|
+
yield event
|
|
308
|
+
if event.result:
|
|
309
|
+
state = event.result
|
|
310
|
+
|
|
311
|
+
# Progress updates
|
|
312
|
+
for i, item in enumerate(state.items):
|
|
313
|
+
yield self.progress("process", i + 1, len(state.items))
|
|
314
|
+
await self.process(item)
|
|
315
|
+
|
|
316
|
+
yield self.ok(Result(count=len(state.items)))
|
|
317
|
+
|
|
318
|
+
async def fetch(self, state: State) -> State:
|
|
319
|
+
return State(items=["a", "b", "c"])
|
|
320
|
+
|
|
321
|
+
async def process(self, item: str):
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
# Run
|
|
325
|
+
orchestrator = MyOrchestrator()
|
|
326
|
+
result = orchestrator.run_sync()
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Configuration
|
|
330
|
+
|
|
331
|
+
Environment variables:
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
# LLM Providers (at least one required)
|
|
335
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
336
|
+
OPENAI_API_KEY=sk-...
|
|
337
|
+
GOOGLE_API_KEY=...
|
|
338
|
+
|
|
339
|
+
# Optional
|
|
340
|
+
AZURE_API_KEY=...
|
|
341
|
+
AZURE_ENDPOINT=https://your-resource.openai.azure.com/
|
|
342
|
+
DEFAULT_MODEL=claude-sonnet-4-20250514
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## Development
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# Setup
|
|
349
|
+
uv sync
|
|
350
|
+
|
|
351
|
+
# Tests
|
|
352
|
+
make test # All
|
|
353
|
+
make test-unit # Unit only
|
|
354
|
+
make test-integration # Integration only
|
|
355
|
+
make test-unit-single PATTERN=name # Single test
|
|
356
|
+
|
|
357
|
+
# Quality
|
|
358
|
+
make lint # Check linting
|
|
359
|
+
make lint-fix # Fix linting
|
|
360
|
+
make typecheck # Type check
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Message Structure
|
|
364
|
+
|
|
365
|
+
Messages contain blocks of different types:
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
from spaik_sdk.thread.models import MessageBlockType
|
|
369
|
+
|
|
370
|
+
# Block types
|
|
371
|
+
MessageBlockType.PLAIN # Regular text
|
|
372
|
+
MessageBlockType.REASONING # Chain of thought
|
|
373
|
+
MessageBlockType.TOOL_USE # Tool call
|
|
374
|
+
MessageBlockType.ERROR # Error message
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## License
|
|
378
|
+
|
|
379
|
+
MIT - Copyright (c) 2025 Siili Solutions Oyj
|