thoughtflow 0.0.1__py3-none-any.whl → 0.0.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.
- thoughtflow/__init__.py +54 -5
- thoughtflow/_util.py +108 -0
- thoughtflow/adapters/__init__.py +43 -0
- thoughtflow/adapters/anthropic.py +119 -0
- thoughtflow/adapters/base.py +140 -0
- thoughtflow/adapters/local.py +133 -0
- thoughtflow/adapters/openai.py +118 -0
- thoughtflow/agent.py +147 -0
- thoughtflow/eval/__init__.py +34 -0
- thoughtflow/eval/harness.py +200 -0
- thoughtflow/eval/replay.py +137 -0
- thoughtflow/memory/__init__.py +27 -0
- thoughtflow/memory/base.py +142 -0
- thoughtflow/message.py +140 -0
- thoughtflow/py.typed +2 -0
- thoughtflow/tools/__init__.py +27 -0
- thoughtflow/tools/base.py +145 -0
- thoughtflow/tools/registry.py +122 -0
- thoughtflow/trace/__init__.py +34 -0
- thoughtflow/trace/events.py +183 -0
- thoughtflow/trace/schema.py +111 -0
- thoughtflow/trace/session.py +141 -0
- thoughtflow-0.0.2.dist-info/METADATA +215 -0
- thoughtflow-0.0.2.dist-info/RECORD +26 -0
- {thoughtflow-0.0.1.dist-info → thoughtflow-0.0.2.dist-info}/WHEEL +1 -2
- {thoughtflow-0.0.1.dist-info → thoughtflow-0.0.2.dist-info/licenses}/LICENSE +1 -1
- thoughtflow/jtools1.py +0 -25
- thoughtflow/jtools2.py +0 -27
- thoughtflow-0.0.1.dist-info/METADATA +0 -17
- thoughtflow-0.0.1.dist-info/RECORD +0 -8
- thoughtflow-0.0.1.dist-info/top_level.txt +0 -1
thoughtflow/__init__.py
CHANGED
|
@@ -1,7 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ThoughtFlow: A minimal, explicit, Pythonic substrate for LLM and agent systems.
|
|
1
3
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
Core Philosophy:
|
|
5
|
+
- Tiny surface area: Few powerful primitives over many specialized classes
|
|
6
|
+
- Explicit state: All state is visible, serializable, and replayable
|
|
7
|
+
- Portability: Works across OpenAI, Anthropic, local models, serverless
|
|
8
|
+
- Deterministic testing: Record/replay workflows, stable sessions
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
from
|
|
10
|
+
Basic Usage:
|
|
11
|
+
>>> from thoughtflow import Agent
|
|
12
|
+
>>> from thoughtflow.adapters import OpenAIAdapter
|
|
13
|
+
>>>
|
|
14
|
+
>>> adapter = OpenAIAdapter(api_key="...")
|
|
15
|
+
>>> agent = Agent(adapter)
|
|
16
|
+
>>> response = agent.call([
|
|
17
|
+
... {"role": "user", "content": "Hello!"}
|
|
18
|
+
... ])
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
# Core exports
|
|
24
|
+
from thoughtflow.agent import Agent
|
|
25
|
+
from thoughtflow.message import Message, MessageList
|
|
26
|
+
|
|
27
|
+
# Submodule access
|
|
28
|
+
from thoughtflow import adapters
|
|
29
|
+
from thoughtflow import tools
|
|
30
|
+
from thoughtflow import memory
|
|
31
|
+
from thoughtflow import trace
|
|
32
|
+
from thoughtflow import eval
|
|
33
|
+
|
|
34
|
+
# Version
|
|
35
|
+
try:
|
|
36
|
+
from importlib.metadata import version as _get_version
|
|
37
|
+
|
|
38
|
+
__version__ = _get_version("thoughtflow")
|
|
39
|
+
except Exception:
|
|
40
|
+
__version__ = "0.0.0"
|
|
41
|
+
|
|
42
|
+
# Public API
|
|
43
|
+
__all__ = [
|
|
44
|
+
# Core
|
|
45
|
+
"Agent",
|
|
46
|
+
"Message",
|
|
47
|
+
"MessageList",
|
|
48
|
+
# Submodules
|
|
49
|
+
"adapters",
|
|
50
|
+
"tools",
|
|
51
|
+
"memory",
|
|
52
|
+
"trace",
|
|
53
|
+
"eval",
|
|
54
|
+
# Metadata
|
|
55
|
+
"__version__",
|
|
56
|
+
]
|
thoughtflow/_util.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Internal utilities for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
This module contains helper functions used internally by ThoughtFlow.
|
|
5
|
+
These are NOT part of the public API and may change without notice.
|
|
6
|
+
|
|
7
|
+
Note: The underscore prefix indicates this is internal/private.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable, TypeVar
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_timestamp() -> float:
|
|
19
|
+
"""Get current timestamp in seconds since epoch.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Current time as a float.
|
|
23
|
+
"""
|
|
24
|
+
return time.time()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_timestamp_ms() -> int:
|
|
28
|
+
"""Get current timestamp in milliseconds since epoch.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Current time in milliseconds as an integer.
|
|
32
|
+
"""
|
|
33
|
+
return int(time.time() * 1000)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
37
|
+
"""Deep merge two dictionaries.
|
|
38
|
+
|
|
39
|
+
Values from `override` take precedence. Nested dicts are merged recursively.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
base: The base dictionary.
|
|
43
|
+
override: The dictionary to merge on top.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A new merged dictionary.
|
|
47
|
+
"""
|
|
48
|
+
result = base.copy()
|
|
49
|
+
for key, value in override.items():
|
|
50
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
51
|
+
result[key] = deep_merge(result[key], value)
|
|
52
|
+
else:
|
|
53
|
+
result[key] = value
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def truncate_string(s: str, max_length: int = 100, suffix: str = "...") -> str:
|
|
58
|
+
"""Truncate a string to a maximum length.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
s: The string to truncate.
|
|
62
|
+
max_length: Maximum length (including suffix).
|
|
63
|
+
suffix: Suffix to add if truncated.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The truncated string.
|
|
67
|
+
"""
|
|
68
|
+
if len(s) <= max_length:
|
|
69
|
+
return s
|
|
70
|
+
return s[: max_length - len(suffix)] + suffix
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def retry_with_backoff(
|
|
74
|
+
func: Callable[[], T],
|
|
75
|
+
max_retries: int = 3,
|
|
76
|
+
base_delay: float = 1.0,
|
|
77
|
+
max_delay: float = 60.0,
|
|
78
|
+
) -> T:
|
|
79
|
+
"""Retry a function with exponential backoff.
|
|
80
|
+
|
|
81
|
+
Note: This is an EXPLICIT retry mechanism - callers must opt-in.
|
|
82
|
+
ThoughtFlow does not retry implicitly.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
func: The function to retry.
|
|
86
|
+
max_retries: Maximum number of retry attempts.
|
|
87
|
+
base_delay: Initial delay in seconds.
|
|
88
|
+
max_delay: Maximum delay between retries.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The function's return value.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
Exception: The last exception if all retries fail.
|
|
95
|
+
"""
|
|
96
|
+
last_exception: Exception | None = None
|
|
97
|
+
delay = base_delay
|
|
98
|
+
|
|
99
|
+
for attempt in range(max_retries + 1):
|
|
100
|
+
try:
|
|
101
|
+
return func()
|
|
102
|
+
except Exception as e:
|
|
103
|
+
last_exception = e
|
|
104
|
+
if attempt < max_retries:
|
|
105
|
+
time.sleep(min(delay, max_delay))
|
|
106
|
+
delay *= 2 # Exponential backoff
|
|
107
|
+
|
|
108
|
+
raise last_exception # type: ignore[misc]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider adapters for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Adapters translate between ThoughtFlow's stable message schema and
|
|
5
|
+
provider-specific APIs (OpenAI, Anthropic, local models, etc.).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from thoughtflow.adapters import OpenAIAdapter
|
|
9
|
+
>>> adapter = OpenAIAdapter(api_key="...")
|
|
10
|
+
>>> response = adapter.complete(messages, params)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from thoughtflow.adapters.base import Adapter, AdapterConfig
|
|
16
|
+
|
|
17
|
+
# Lazy imports to avoid requiring all provider dependencies
|
|
18
|
+
# Users only need to install the providers they use
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Adapter",
|
|
22
|
+
"AdapterConfig",
|
|
23
|
+
"OpenAIAdapter",
|
|
24
|
+
"AnthropicAdapter",
|
|
25
|
+
"LocalAdapter",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def __getattr__(name: str):
|
|
30
|
+
"""Lazy load adapters to avoid import errors for missing dependencies."""
|
|
31
|
+
if name == "OpenAIAdapter":
|
|
32
|
+
from thoughtflow.adapters.openai import OpenAIAdapter
|
|
33
|
+
|
|
34
|
+
return OpenAIAdapter
|
|
35
|
+
elif name == "AnthropicAdapter":
|
|
36
|
+
from thoughtflow.adapters.anthropic import AnthropicAdapter
|
|
37
|
+
|
|
38
|
+
return AnthropicAdapter
|
|
39
|
+
elif name == "LocalAdapter":
|
|
40
|
+
from thoughtflow.adapters.local import LocalAdapter
|
|
41
|
+
|
|
42
|
+
return LocalAdapter
|
|
43
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Anthropic adapter for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Provides integration with Anthropic's API (Claude models).
|
|
5
|
+
|
|
6
|
+
Requires: pip install thoughtflow[anthropic]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from thoughtflow.adapters.base import Adapter, AdapterConfig, AdapterResponse
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from thoughtflow.message import MessageList
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AnthropicAdapter(Adapter):
|
|
20
|
+
"""Adapter for Anthropic's API.
|
|
21
|
+
|
|
22
|
+
Supports Claude 3, Claude 2, and other Anthropic models.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> adapter = AnthropicAdapter(api_key="sk-ant-...")
|
|
26
|
+
>>> response = adapter.complete([
|
|
27
|
+
... {"role": "user", "content": "Hello!"}
|
|
28
|
+
... ])
|
|
29
|
+
>>> print(response.content)
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
config: Adapter configuration.
|
|
33
|
+
client: Anthropic client instance (created lazily).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
DEFAULT_MODEL = "claude-sonnet-4-20250514"
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
api_key: str | None = None,
|
|
41
|
+
config: AdapterConfig | None = None,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""Initialize the Anthropic adapter.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
api_key: Anthropic API key. Can also be set via ANTHROPIC_API_KEY env var.
|
|
48
|
+
config: Full adapter configuration.
|
|
49
|
+
**kwargs: Additional config options.
|
|
50
|
+
"""
|
|
51
|
+
if config is None:
|
|
52
|
+
config = AdapterConfig(api_key=api_key, **kwargs)
|
|
53
|
+
super().__init__(config)
|
|
54
|
+
self._client = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def client(self) -> Any:
|
|
58
|
+
"""Lazy-load the Anthropic client.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Anthropic client instance.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ImportError: If anthropic package is not installed.
|
|
65
|
+
"""
|
|
66
|
+
if self._client is None:
|
|
67
|
+
try:
|
|
68
|
+
from anthropic import Anthropic
|
|
69
|
+
except ImportError as e:
|
|
70
|
+
raise ImportError(
|
|
71
|
+
"Anthropic package not installed. "
|
|
72
|
+
"Install with: pip install thoughtflow[anthropic]"
|
|
73
|
+
) from e
|
|
74
|
+
|
|
75
|
+
self._client = Anthropic(
|
|
76
|
+
api_key=self.config.api_key,
|
|
77
|
+
base_url=self.config.base_url,
|
|
78
|
+
timeout=self.config.timeout,
|
|
79
|
+
max_retries=self.config.max_retries,
|
|
80
|
+
)
|
|
81
|
+
return self._client
|
|
82
|
+
|
|
83
|
+
def complete(
|
|
84
|
+
self,
|
|
85
|
+
messages: MessageList,
|
|
86
|
+
params: dict[str, Any] | None = None,
|
|
87
|
+
) -> AdapterResponse:
|
|
88
|
+
"""Generate a completion using Anthropic's API.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
messages: List of message dicts.
|
|
92
|
+
params: Optional parameters (model, temperature, max_tokens, etc.)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
AdapterResponse with the generated content.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
NotImplementedError: This is a placeholder implementation.
|
|
99
|
+
"""
|
|
100
|
+
# TODO: Implement actual Anthropic API call
|
|
101
|
+
# Note: Anthropic uses a different message format (system as separate param)
|
|
102
|
+
raise NotImplementedError(
|
|
103
|
+
"AnthropicAdapter.complete() is not yet implemented. "
|
|
104
|
+
"This is a placeholder for the ThoughtFlow alpha release."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def get_capabilities(self) -> dict[str, Any]:
|
|
108
|
+
"""Get Anthropic adapter capabilities.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Dict of supported features.
|
|
112
|
+
"""
|
|
113
|
+
return {
|
|
114
|
+
"streaming": True,
|
|
115
|
+
"tool_calling": True,
|
|
116
|
+
"vision": True,
|
|
117
|
+
"json_mode": False, # Anthropic doesn't have native JSON mode
|
|
118
|
+
"seed": False,
|
|
119
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base adapter interface for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
All provider adapters implement this interface, ensuring a stable
|
|
5
|
+
contract across different LLM providers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from thoughtflow.message import MessageList
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class AdapterConfig:
|
|
20
|
+
"""Configuration for an adapter.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
api_key: API key for the provider (if required).
|
|
24
|
+
base_url: Optional custom base URL for the API.
|
|
25
|
+
timeout: Request timeout in seconds.
|
|
26
|
+
max_retries: Maximum number of retries for failed requests.
|
|
27
|
+
default_model: Default model to use if not specified in params.
|
|
28
|
+
extra: Additional provider-specific configuration.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
api_key: str | None = None
|
|
32
|
+
base_url: str | None = None
|
|
33
|
+
timeout: float = 60.0
|
|
34
|
+
max_retries: int = 3
|
|
35
|
+
default_model: str | None = None
|
|
36
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class AdapterResponse:
|
|
41
|
+
"""Response from an adapter completion call.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
content: The generated text content.
|
|
45
|
+
model: The model that generated the response.
|
|
46
|
+
usage: Token usage information (prompt, completion, total).
|
|
47
|
+
finish_reason: Why the model stopped (stop, length, tool_calls, etc.).
|
|
48
|
+
raw: The raw response from the provider (for debugging).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
content: str
|
|
52
|
+
model: str | None = None
|
|
53
|
+
usage: dict[str, int] | None = None
|
|
54
|
+
finish_reason: str | None = None
|
|
55
|
+
raw: Any = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Adapter(ABC):
|
|
59
|
+
"""Abstract base class for provider adapters.
|
|
60
|
+
|
|
61
|
+
Adapters are responsible for:
|
|
62
|
+
- Translating ThoughtFlow's message format to provider-specific format
|
|
63
|
+
- Making API calls to the provider
|
|
64
|
+
- Translating responses back to ThoughtFlow's format
|
|
65
|
+
- Handling provider-specific errors and retries
|
|
66
|
+
|
|
67
|
+
Subclasses must implement:
|
|
68
|
+
- `complete()`: Synchronous completion
|
|
69
|
+
- `complete_async()`: Asynchronous completion (optional)
|
|
70
|
+
- `get_capabilities()`: Report adapter capabilities
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, config: AdapterConfig | None = None, **kwargs: Any) -> None:
|
|
74
|
+
"""Initialize the adapter.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
config: Adapter configuration object.
|
|
78
|
+
**kwargs: Shorthand for config fields (api_key, base_url, etc.)
|
|
79
|
+
"""
|
|
80
|
+
if config is None:
|
|
81
|
+
config = AdapterConfig(**kwargs)
|
|
82
|
+
self.config = config
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def complete(
|
|
86
|
+
self,
|
|
87
|
+
messages: MessageList,
|
|
88
|
+
params: dict[str, Any] | None = None,
|
|
89
|
+
) -> AdapterResponse:
|
|
90
|
+
"""Generate a completion for the given messages.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
messages: List of message dicts.
|
|
94
|
+
params: Optional parameters (model, temperature, max_tokens, etc.)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
AdapterResponse with the generated content.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
NotImplementedError: Subclasses must implement this method.
|
|
101
|
+
"""
|
|
102
|
+
raise NotImplementedError
|
|
103
|
+
|
|
104
|
+
async def complete_async(
|
|
105
|
+
self,
|
|
106
|
+
messages: MessageList,
|
|
107
|
+
params: dict[str, Any] | None = None,
|
|
108
|
+
) -> AdapterResponse:
|
|
109
|
+
"""Async version of complete().
|
|
110
|
+
|
|
111
|
+
Default implementation calls the sync version.
|
|
112
|
+
Override for true async support.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
messages: List of message dicts.
|
|
116
|
+
params: Optional parameters.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
AdapterResponse with the generated content.
|
|
120
|
+
"""
|
|
121
|
+
# Default: fall back to sync
|
|
122
|
+
return self.complete(messages, params)
|
|
123
|
+
|
|
124
|
+
def get_capabilities(self) -> dict[str, Any]:
|
|
125
|
+
"""Get the capabilities of this adapter.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dict describing what this adapter supports:
|
|
129
|
+
- streaming: bool
|
|
130
|
+
- tool_calling: bool
|
|
131
|
+
- vision: bool
|
|
132
|
+
- json_mode: bool
|
|
133
|
+
- etc.
|
|
134
|
+
"""
|
|
135
|
+
return {
|
|
136
|
+
"streaming": False,
|
|
137
|
+
"tool_calling": False,
|
|
138
|
+
"vision": False,
|
|
139
|
+
"json_mode": False,
|
|
140
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local model adapter for ThoughtFlow.
|
|
3
|
+
|
|
4
|
+
Provides integration with locally-running models via Ollama, LM Studio,
|
|
5
|
+
or other local inference servers.
|
|
6
|
+
|
|
7
|
+
Requires: pip install thoughtflow[local]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from thoughtflow.adapters.base import Adapter, AdapterConfig, AdapterResponse
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from thoughtflow.message import MessageList
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LocalAdapter(Adapter):
|
|
21
|
+
"""Adapter for locally-running models.
|
|
22
|
+
|
|
23
|
+
Supports Ollama, LM Studio, and other OpenAI-compatible local servers.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> # Using Ollama
|
|
27
|
+
>>> adapter = LocalAdapter(base_url="http://localhost:11434/v1")
|
|
28
|
+
>>> response = adapter.complete([
|
|
29
|
+
... {"role": "user", "content": "Hello!"}
|
|
30
|
+
... ], params={"model": "llama3"})
|
|
31
|
+
|
|
32
|
+
>>> # Using LM Studio
|
|
33
|
+
>>> adapter = LocalAdapter(base_url="http://localhost:1234/v1")
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
config: Adapter configuration.
|
|
37
|
+
client: HTTP client for making requests.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
DEFAULT_BASE_URL = "http://localhost:11434/v1"
|
|
41
|
+
DEFAULT_MODEL = "llama3"
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
base_url: str | None = None,
|
|
46
|
+
config: AdapterConfig | None = None,
|
|
47
|
+
**kwargs: Any,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize the local adapter.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
base_url: URL of the local inference server.
|
|
53
|
+
config: Full adapter configuration.
|
|
54
|
+
**kwargs: Additional config options.
|
|
55
|
+
"""
|
|
56
|
+
if config is None:
|
|
57
|
+
config = AdapterConfig(
|
|
58
|
+
base_url=base_url or self.DEFAULT_BASE_URL,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
|
61
|
+
super().__init__(config)
|
|
62
|
+
self._client = None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def client(self) -> Any:
|
|
66
|
+
"""Lazy-load the HTTP client.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Ollama client or httpx client instance.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
ImportError: If required packages are not installed.
|
|
73
|
+
"""
|
|
74
|
+
if self._client is None:
|
|
75
|
+
# Try Ollama first, fall back to generic OpenAI-compatible client
|
|
76
|
+
try:
|
|
77
|
+
from ollama import Client
|
|
78
|
+
|
|
79
|
+
self._client = Client(host=self.config.base_url)
|
|
80
|
+
except ImportError:
|
|
81
|
+
# Fall back to using OpenAI client with custom base_url
|
|
82
|
+
try:
|
|
83
|
+
from openai import OpenAI
|
|
84
|
+
|
|
85
|
+
self._client = OpenAI(
|
|
86
|
+
base_url=self.config.base_url,
|
|
87
|
+
api_key="not-needed", # Local servers often don't need keys
|
|
88
|
+
)
|
|
89
|
+
except ImportError as e:
|
|
90
|
+
raise ImportError(
|
|
91
|
+
"No local model client available. "
|
|
92
|
+
"Install with: pip install thoughtflow[local] or thoughtflow[openai]"
|
|
93
|
+
) from e
|
|
94
|
+
return self._client
|
|
95
|
+
|
|
96
|
+
def complete(
|
|
97
|
+
self,
|
|
98
|
+
messages: MessageList,
|
|
99
|
+
params: dict[str, Any] | None = None,
|
|
100
|
+
) -> AdapterResponse:
|
|
101
|
+
"""Generate a completion using a local model.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
messages: List of message dicts.
|
|
105
|
+
params: Optional parameters (model, temperature, etc.)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
AdapterResponse with the generated content.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
NotImplementedError: This is a placeholder implementation.
|
|
112
|
+
"""
|
|
113
|
+
# TODO: Implement actual local model call
|
|
114
|
+
raise NotImplementedError(
|
|
115
|
+
"LocalAdapter.complete() is not yet implemented. "
|
|
116
|
+
"This is a placeholder for the ThoughtFlow alpha release."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_capabilities(self) -> dict[str, Any]:
|
|
120
|
+
"""Get local adapter capabilities.
|
|
121
|
+
|
|
122
|
+
Note: Capabilities depend on the specific model being used.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Dict of supported features.
|
|
126
|
+
"""
|
|
127
|
+
return {
|
|
128
|
+
"streaming": True,
|
|
129
|
+
"tool_calling": False, # Depends on model
|
|
130
|
+
"vision": False, # Depends on model
|
|
131
|
+
"json_mode": False,
|
|
132
|
+
"seed": True,
|
|
133
|
+
}
|