alpineagents 0.1.0__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.
- alpineagents/__init__.py +81 -0
- alpineagents/_async.py +139 -0
- alpineagents/_runner.py +552 -0
- alpineagents/_structured.py +175 -0
- alpineagents/_tokens.py +61 -0
- alpineagents/agent.py +1196 -0
- alpineagents/blocks.py +51 -0
- alpineagents/errors.py +79 -0
- alpineagents/human.py +141 -0
- alpineagents/loop.py +337 -0
- alpineagents/mcp_tools.py +507 -0
- alpineagents/models/__init__.py +8 -0
- alpineagents/models/anthropic.py +520 -0
- alpineagents/models/base.py +215 -0
- alpineagents/models/openai_compatible.py +493 -0
- alpineagents/models/resolve.py +95 -0
- alpineagents/reporter.py +89 -0
- alpineagents/state.py +1097 -0
- alpineagents/terminal.py +366 -0
- alpineagents/testing.py +236 -0
- alpineagents/tool.py +626 -0
- alpineagents/types.py +410 -0
- alpineagents-0.1.0.dist-info/METADATA +200 -0
- alpineagents-0.1.0.dist-info/RECORD +26 -0
- alpineagents-0.1.0.dist-info/WHEEL +4 -0
- alpineagents-0.1.0.dist-info/licenses/LICENSE +21 -0
alpineagents/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""alpineagents: an agent looks at the state, thinks, and uses tools when needed, until there is an answer."""
|
|
2
|
+
|
|
3
|
+
from .agent import Agent
|
|
4
|
+
from .blocks import CompactIfFull, acompact_if_full, compact_if_full
|
|
5
|
+
from .errors import (
|
|
6
|
+
AlpineAgentsError,
|
|
7
|
+
AuthError,
|
|
8
|
+
ContextTooLongError,
|
|
9
|
+
MCPConnectionError,
|
|
10
|
+
NoHumanError,
|
|
11
|
+
OutputError,
|
|
12
|
+
ProviderError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
)
|
|
15
|
+
from .human import Human
|
|
16
|
+
from .loop import Loop, adefault_loop, default_loop, loop
|
|
17
|
+
from .mcp_tools import MCP
|
|
18
|
+
from .models import Anthropic, Model, OpenAICompatible
|
|
19
|
+
from .reporter import Reporter
|
|
20
|
+
from .state import State
|
|
21
|
+
from .terminal import Terminal
|
|
22
|
+
from .tool import Tool, tool
|
|
23
|
+
from .types import (
|
|
24
|
+
ContextChange,
|
|
25
|
+
HistoryEntry,
|
|
26
|
+
Message,
|
|
27
|
+
ModelEvent,
|
|
28
|
+
Price,
|
|
29
|
+
Reply,
|
|
30
|
+
Request,
|
|
31
|
+
ToolCall,
|
|
32
|
+
ToolOutcome,
|
|
33
|
+
ToolSpec,
|
|
34
|
+
Usage,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# Objects
|
|
41
|
+
"Agent",
|
|
42
|
+
"State",
|
|
43
|
+
"loop",
|
|
44
|
+
"Loop",
|
|
45
|
+
"default_loop",
|
|
46
|
+
"adefault_loop",
|
|
47
|
+
"tool",
|
|
48
|
+
"Tool",
|
|
49
|
+
"compact_if_full",
|
|
50
|
+
"acompact_if_full",
|
|
51
|
+
"MCP",
|
|
52
|
+
"CompactIfFull",
|
|
53
|
+
# Roles and implementations
|
|
54
|
+
"Model",
|
|
55
|
+
"Anthropic",
|
|
56
|
+
"OpenAICompatible",
|
|
57
|
+
"Reporter",
|
|
58
|
+
"Human",
|
|
59
|
+
"Terminal",
|
|
60
|
+
# Data
|
|
61
|
+
"Price",
|
|
62
|
+
"Usage",
|
|
63
|
+
"ToolCall",
|
|
64
|
+
"Reply",
|
|
65
|
+
"Request",
|
|
66
|
+
"Message",
|
|
67
|
+
"ToolSpec",
|
|
68
|
+
"HistoryEntry",
|
|
69
|
+
"ContextChange",
|
|
70
|
+
"ModelEvent",
|
|
71
|
+
"ToolOutcome",
|
|
72
|
+
# Errors
|
|
73
|
+
"AlpineAgentsError",
|
|
74
|
+
"ProviderError",
|
|
75
|
+
"RateLimitError",
|
|
76
|
+
"ContextTooLongError",
|
|
77
|
+
"AuthError",
|
|
78
|
+
"OutputError",
|
|
79
|
+
"NoHumanError",
|
|
80
|
+
"MCPConnectionError",
|
|
81
|
+
]
|
alpineagents/_async.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Helpers the async API shares (ARCHITECTURE.md "Async API").
|
|
2
|
+
|
|
3
|
+
- ``run_in_thread``: runs a blocking function on a daemon thread and returns an awaitable (not
|
|
4
|
+
``asyncio.to_thread``: its pool is joined when ``asyncio.run`` ends, so a call that was cancelled and not waited
|
|
5
|
+
for would hold the program until it finishes).
|
|
6
|
+
- ``Relay``: calls callbacks that a worker thread invokes (``on_text``/``on_event``) on the event loop thread and
|
|
7
|
+
waits for them, so their exceptions reach the worker thread as is.
|
|
8
|
+
- ``ASYNC_RUN`` and ``check_sync_call``: a sync Agent method called on the event loop thread of an async run would
|
|
9
|
+
block every other task, so it is reported with the async name to use instead.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import contextvars
|
|
16
|
+
import inspect
|
|
17
|
+
import threading
|
|
18
|
+
from collections.abc import Awaitable, Callable
|
|
19
|
+
from concurrent.futures import Future, wait
|
|
20
|
+
from typing import Any, TypeVar
|
|
21
|
+
|
|
22
|
+
from .errors import fix_message
|
|
23
|
+
|
|
24
|
+
__all__ = ["ASYNC_RUN", "Relay", "check_sync_call", "is_async_callable", "run_in_thread"]
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
#: The event loop running the current async run (set by ``Agent.arun`` and async loops). ``None`` otherwise.
|
|
29
|
+
ASYNC_RUN: contextvars.ContextVar[asyncio.AbstractEventLoop | None] = contextvars.ContextVar(
|
|
30
|
+
"alpineagents_async_run", default=None
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
#: How long (seconds) a worker thread waits at a time for a relayed callback, so it notices a cancel.
|
|
34
|
+
_POLL = 0.1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_in_thread(fn: Callable[..., T], /, *args: Any) -> Awaitable[T]:
|
|
38
|
+
"""Runs ``fn(*args)`` on a new daemon thread, with a copy of the current context. Cancelling the awaitable does
|
|
39
|
+
not stop the thread; the result is dropped."""
|
|
40
|
+
future: Future[T] = Future()
|
|
41
|
+
context = contextvars.copy_context()
|
|
42
|
+
|
|
43
|
+
def target() -> None:
|
|
44
|
+
if not future.set_running_or_notify_cancel():
|
|
45
|
+
return
|
|
46
|
+
try:
|
|
47
|
+
result = context.run(fn, *args)
|
|
48
|
+
except BaseException as e:
|
|
49
|
+
future.set_exception(e)
|
|
50
|
+
else:
|
|
51
|
+
future.set_result(result)
|
|
52
|
+
|
|
53
|
+
threading.Thread(target=target, name="alpineagents-worker", daemon=True).start()
|
|
54
|
+
return asyncio.wrap_future(future)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Relay:
|
|
58
|
+
"""Wraps callbacks so a worker thread runs them on ``loop``'s thread and gets their result or exception.
|
|
59
|
+
|
|
60
|
+
After ``close()`` (the awaiting side finished or was cancelled), a wrapped callback raises
|
|
61
|
+
``CancelledError`` in the worker thread instead of running, which also ends a stream early.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
65
|
+
self._loop = loop
|
|
66
|
+
self._closed = False
|
|
67
|
+
self._lock = threading.Lock()
|
|
68
|
+
|
|
69
|
+
def wrap(self, callback: Callable[..., Any] | None) -> Callable[..., Any] | None:
|
|
70
|
+
if callback is None:
|
|
71
|
+
return None
|
|
72
|
+
return lambda *args: self._call(callback, args)
|
|
73
|
+
|
|
74
|
+
def close(self) -> None:
|
|
75
|
+
with self._lock:
|
|
76
|
+
self._closed = True
|
|
77
|
+
|
|
78
|
+
def _call(self, callback: Callable[..., Any], args: tuple[Any, ...]) -> Any:
|
|
79
|
+
done: Future[Any] = Future()
|
|
80
|
+
|
|
81
|
+
def run() -> None:
|
|
82
|
+
# close() runs on this (the loop's) thread, so a callback queued before it is dropped here.
|
|
83
|
+
if self._closed:
|
|
84
|
+
done.cancel()
|
|
85
|
+
return
|
|
86
|
+
if not done.set_running_or_notify_cancel():
|
|
87
|
+
return
|
|
88
|
+
try:
|
|
89
|
+
done.set_result(callback(*args))
|
|
90
|
+
except BaseException as e:
|
|
91
|
+
done.set_exception(e)
|
|
92
|
+
|
|
93
|
+
with self._lock:
|
|
94
|
+
if self._closed:
|
|
95
|
+
raise asyncio.CancelledError()
|
|
96
|
+
try:
|
|
97
|
+
self._loop.call_soon_threadsafe(run)
|
|
98
|
+
except RuntimeError: # the loop is closed
|
|
99
|
+
raise asyncio.CancelledError() from None
|
|
100
|
+
# Poll with wait(), not result(timeout=): a TimeoutError raised by the callback must propagate as is.
|
|
101
|
+
while not wait([done], timeout=_POLL).done:
|
|
102
|
+
if self._closed:
|
|
103
|
+
done.cancel()
|
|
104
|
+
raise asyncio.CancelledError()
|
|
105
|
+
if done.cancelled():
|
|
106
|
+
raise asyncio.CancelledError()
|
|
107
|
+
return done.result()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def check_sync_call(method: str, hint: str = "") -> None:
|
|
111
|
+
"""``TypeError`` if ``agent.{method}()`` (sync) is called on the event loop thread of an async run.
|
|
112
|
+
|
|
113
|
+
The sync API still works where a loop is running but no async run is (Jupyter), and on worker threads.
|
|
114
|
+
"""
|
|
115
|
+
run_loop = ASYNC_RUN.get()
|
|
116
|
+
if run_loop is None:
|
|
117
|
+
return
|
|
118
|
+
try:
|
|
119
|
+
running = asyncio.get_running_loop()
|
|
120
|
+
except RuntimeError:
|
|
121
|
+
return
|
|
122
|
+
if running is run_loop:
|
|
123
|
+
raise TypeError(
|
|
124
|
+
fix_message(
|
|
125
|
+
f"agent.{method}() was called inside an async run, where it would block the event loop",
|
|
126
|
+
f"Use the async version, await agent.a{method}(...){hint}",
|
|
127
|
+
f"await agent.a{method}(state)" if method != "run" else "answer = await agent.arun(state)",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def is_async_callable(obj: Any) -> bool:
|
|
133
|
+
"""Whether calling ``obj`` returns an awaitable: an ``async def`` function (or a partial of one), an async
|
|
134
|
+
``@loop`` (``is_async``), or an object whose ``__call__`` is ``async def``."""
|
|
135
|
+
if inspect.iscoroutinefunction(obj):
|
|
136
|
+
return True
|
|
137
|
+
if getattr(obj, "is_async", False) is True:
|
|
138
|
+
return True
|
|
139
|
+
return inspect.iscoroutinefunction(getattr(type(obj), "__call__", None))
|