stirrup 0.1.2__py3-none-any.whl → 0.1.4__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.
- stirrup/__init__.py +2 -0
- stirrup/clients/__init__.py +5 -0
- stirrup/clients/chat_completions_client.py +0 -3
- stirrup/clients/litellm_client.py +20 -11
- stirrup/clients/open_responses_client.py +434 -0
- stirrup/clients/utils.py +6 -1
- stirrup/constants.py +6 -2
- stirrup/core/agent.py +196 -57
- stirrup/core/cache.py +479 -0
- stirrup/core/models.py +53 -9
- stirrup/prompts/base_system_prompt.txt +1 -1
- stirrup/tools/__init__.py +3 -0
- stirrup/tools/browser_use.py +591 -0
- stirrup/tools/calculator.py +1 -1
- stirrup/tools/code_backends/base.py +24 -0
- stirrup/tools/code_backends/docker.py +19 -0
- stirrup/tools/code_backends/e2b.py +43 -11
- stirrup/tools/code_backends/local.py +19 -2
- stirrup/tools/finish.py +27 -1
- stirrup/tools/user_input.py +130 -0
- stirrup/tools/web.py +1 -0
- stirrup/utils/logging.py +32 -7
- {stirrup-0.1.2.dist-info → stirrup-0.1.4.dist-info}/METADATA +16 -13
- stirrup-0.1.4.dist-info/RECORD +38 -0
- {stirrup-0.1.2.dist-info → stirrup-0.1.4.dist-info}/WHEEL +2 -2
- stirrup-0.1.2.dist-info/RECORD +0 -34
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
5
7
|
try:
|
|
6
8
|
from e2b import InvalidArgumentException, TimeoutException
|
|
7
9
|
from e2b.sandbox.filesystem.filesystem import FileType
|
|
@@ -13,7 +15,7 @@ except ImportError as e:
|
|
|
13
15
|
|
|
14
16
|
import logging
|
|
15
17
|
|
|
16
|
-
from stirrup.constants import
|
|
18
|
+
from stirrup.constants import SANDBOX_REQUEST_TIMEOUT, SANDBOX_TIMEOUT
|
|
17
19
|
from stirrup.core.models import ImageContentBlock, Tool, ToolUseCountMetadata
|
|
18
20
|
|
|
19
21
|
from .base import (
|
|
@@ -51,7 +53,8 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
51
53
|
def __init__(
|
|
52
54
|
self,
|
|
53
55
|
*,
|
|
54
|
-
timeout: int =
|
|
56
|
+
timeout: int = SANDBOX_TIMEOUT,
|
|
57
|
+
request_timeout: int = SANDBOX_REQUEST_TIMEOUT,
|
|
55
58
|
template: str | None = None,
|
|
56
59
|
allowed_commands: list[str] | None = None,
|
|
57
60
|
) -> None:
|
|
@@ -67,6 +70,7 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
67
70
|
"""
|
|
68
71
|
super().__init__(allowed_commands=allowed_commands)
|
|
69
72
|
self._timeout = timeout
|
|
73
|
+
self._request_timeout = request_timeout
|
|
70
74
|
self._template = template
|
|
71
75
|
self._sbx: AsyncSandbox | None = None
|
|
72
76
|
|
|
@@ -109,7 +113,7 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
109
113
|
if not await self._sbx.files.exists(path):
|
|
110
114
|
raise FileNotFoundError(f"File not found: {path}")
|
|
111
115
|
|
|
112
|
-
file_bytes = await self._sbx.files.read(path, format="bytes")
|
|
116
|
+
file_bytes = await self._sbx.files.read(path, format="bytes", request_timeout=self._request_timeout)
|
|
113
117
|
return bytes(file_bytes)
|
|
114
118
|
|
|
115
119
|
async def write_file_bytes(self, path: str, content: bytes) -> None:
|
|
@@ -126,7 +130,25 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
126
130
|
if self._sbx is None:
|
|
127
131
|
raise RuntimeError("ExecutionEnvironment not started.")
|
|
128
132
|
|
|
129
|
-
await self._sbx.files.write(path, content)
|
|
133
|
+
await self._sbx.files.write(path, content, request_timeout=self._request_timeout)
|
|
134
|
+
|
|
135
|
+
async def file_exists(self, path: str) -> bool:
|
|
136
|
+
"""Check if a file exists in the E2B sandbox.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
path: File path within the sandbox.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
True if the file exists, False otherwise.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
RuntimeError: If environment not started.
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
if self._sbx is None:
|
|
149
|
+
raise RuntimeError("ExecutionEnvironment not started.")
|
|
150
|
+
|
|
151
|
+
return await self._sbx.files.exists(path)
|
|
130
152
|
|
|
131
153
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
132
154
|
"""Execute command in E2B execution environment, returning raw CommandResult."""
|
|
@@ -146,7 +168,7 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
146
168
|
)
|
|
147
169
|
|
|
148
170
|
try:
|
|
149
|
-
r = await self._sbx.commands.run(cmd, timeout=timeout)
|
|
171
|
+
r = await self._sbx.commands.run(cmd, timeout=timeout, request_timeout=self._request_timeout)
|
|
150
172
|
|
|
151
173
|
return CommandResult(
|
|
152
174
|
exit_code=getattr(r, "exit_code", 0),
|
|
@@ -231,7 +253,7 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
231
253
|
continue
|
|
232
254
|
|
|
233
255
|
# Read file content from execution environment
|
|
234
|
-
file_bytes = await self._sbx.files.read(env_path, format="bytes")
|
|
256
|
+
file_bytes = await self._sbx.files.read(env_path, format="bytes", request_timeout=self._request_timeout)
|
|
235
257
|
content = bytes(file_bytes)
|
|
236
258
|
|
|
237
259
|
# Save with original filename directly in output_dir
|
|
@@ -297,6 +319,7 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
297
319
|
result = UploadFilesResult()
|
|
298
320
|
|
|
299
321
|
for source in paths:
|
|
322
|
+
original_name = Path(source).name # Get name BEFORE resolve
|
|
300
323
|
source = Path(source).resolve()
|
|
301
324
|
|
|
302
325
|
if not source.exists():
|
|
@@ -306,9 +329,10 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
306
329
|
|
|
307
330
|
try:
|
|
308
331
|
if source.is_file():
|
|
309
|
-
|
|
332
|
+
source = Path(source).resolve()
|
|
333
|
+
dest = f"{dest_base}/{original_name}"
|
|
310
334
|
content = source.read_bytes()
|
|
311
|
-
await self._sbx.files.write(dest, content)
|
|
335
|
+
await self._sbx.files.write(dest, content, request_timeout=self._request_timeout)
|
|
312
336
|
result.uploaded.append(
|
|
313
337
|
UploadedFile(
|
|
314
338
|
source_path=source,
|
|
@@ -327,7 +351,7 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
327
351
|
relative = file_path.relative_to(source)
|
|
328
352
|
dest = f"{dest_base}/{relative}" if dest_dir else f"{dest_base}/{source.name}/{relative}"
|
|
329
353
|
content = file_path.read_bytes()
|
|
330
|
-
await self._sbx.files.write(dest, content)
|
|
354
|
+
await self._sbx.files.write(dest, content, request_timeout=self._request_timeout)
|
|
331
355
|
result.uploaded.append(
|
|
332
356
|
UploadedFile(
|
|
333
357
|
source_path=file_path,
|
|
@@ -360,5 +384,13 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
360
384
|
FileNotFoundError: If file does not exist.
|
|
361
385
|
|
|
362
386
|
"""
|
|
363
|
-
|
|
364
|
-
|
|
387
|
+
if not path.lower().endswith((".png", ".jpg", ".jpeg")):
|
|
388
|
+
raise ValueError(f"Unsupported image type for `{path}`. Only .png, .jpg, or .jpeg are allowed.")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
file_bytes = await self.read_file_bytes(path)
|
|
392
|
+
image = ImageContentBlock(data=file_bytes)
|
|
393
|
+
except ValidationError as e:
|
|
394
|
+
raise ValueError("You submitted a corrupt/unsupported image file.") from e
|
|
395
|
+
|
|
396
|
+
return image
|
|
@@ -122,7 +122,7 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
122
122
|
if self._temp_base_dir:
|
|
123
123
|
self._temp_base_dir.mkdir(parents=True, exist_ok=True)
|
|
124
124
|
self._temp_dir = Path(tempfile.mkdtemp(prefix="local_exec_env_", dir=self._temp_base_dir))
|
|
125
|
-
logger.
|
|
125
|
+
logger.debug("Created local execution environment temp directory: %s", self._temp_dir)
|
|
126
126
|
return self.get_code_exec_tool(description=self._description)
|
|
127
127
|
|
|
128
128
|
async def __aexit__(
|
|
@@ -205,6 +205,23 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
205
205
|
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
206
206
|
resolved.write_bytes(content)
|
|
207
207
|
|
|
208
|
+
async def file_exists(self, path: str) -> bool:
|
|
209
|
+
"""Check if a file exists in the temp directory.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
path: File path (relative or absolute within the temp dir).
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if the file exists, False otherwise.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
RuntimeError: If environment not started.
|
|
219
|
+
ValueError: If path is outside temp directory.
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
resolved = self._resolve_and_validate_path(path)
|
|
223
|
+
return resolved.exists() and resolved.is_file()
|
|
224
|
+
|
|
208
225
|
async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> CommandResult:
|
|
209
226
|
"""Execute command in the temp directory.
|
|
210
227
|
|
|
@@ -359,7 +376,7 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
359
376
|
|
|
360
377
|
# Move file (overwrites if exists)
|
|
361
378
|
shutil.move(str(source_path), str(dest_path))
|
|
362
|
-
logger.
|
|
379
|
+
logger.debug("Moved file: %s -> %s", source_path, dest_path)
|
|
363
380
|
|
|
364
381
|
result.saved.append(
|
|
365
382
|
SavedFile(
|
stirrup/tools/finish.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""Simple finish tool with file existence validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
1
5
|
from typing import Annotated
|
|
2
6
|
|
|
3
7
|
from pydantic import BaseModel, Field
|
|
@@ -15,9 +19,31 @@ class FinishParams(BaseModel):
|
|
|
15
19
|
]
|
|
16
20
|
|
|
17
21
|
|
|
22
|
+
async def _validating_finish_executor(params: FinishParams) -> ToolResult[ToolUseCountMetadata]:
|
|
23
|
+
"""Validates all reported files exist before completing."""
|
|
24
|
+
from stirrup.core.agent import _SESSION_STATE
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
state = _SESSION_STATE.get(None)
|
|
28
|
+
exec_env = state.exec_env if state else None
|
|
29
|
+
except LookupError:
|
|
30
|
+
exec_env = None
|
|
31
|
+
|
|
32
|
+
if exec_env and params.paths:
|
|
33
|
+
missing = [p for p in params.paths if not await exec_env.file_exists(p)]
|
|
34
|
+
if missing:
|
|
35
|
+
return ToolResult(
|
|
36
|
+
content=f"ERROR: Files do not exist: {missing}. Verify paths and ensure files were saved.",
|
|
37
|
+
metadata=ToolUseCountMetadata(),
|
|
38
|
+
success=False,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return ToolResult(content=params.reason, metadata=ToolUseCountMetadata(), success=True)
|
|
42
|
+
|
|
43
|
+
|
|
18
44
|
SIMPLE_FINISH_TOOL: Tool[FinishParams, ToolUseCountMetadata] = Tool[FinishParams, ToolUseCountMetadata](
|
|
19
45
|
name=FINISH_TOOL_NAME,
|
|
20
46
|
description="Signal task completion with a reason. Use when the task is finished or cannot proceed further. Note that you will need a separate turn to finish.",
|
|
21
47
|
parameters=FinishParams,
|
|
22
|
-
executor=
|
|
48
|
+
executor=_validating_finish_executor,
|
|
23
49
|
)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""User input tool for interactive clarification during agent execution.
|
|
2
|
+
|
|
3
|
+
This module provides the user_input tool that allows agents to ask questions
|
|
4
|
+
and receive text responses from users during task execution.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
from stirrup.clients.chat_completions_client import ChatCompletionsClient
|
|
8
|
+
from stirrup.tools import DEFAULT_TOOLS, USER_INPUT_TOOL
|
|
9
|
+
|
|
10
|
+
client = ChatCompletionsClient(model="gpt-5")
|
|
11
|
+
agent = Agent(
|
|
12
|
+
client=client,
|
|
13
|
+
name="assistant",
|
|
14
|
+
tools=[*DEFAULT_TOOLS, USER_INPUT_TOOL],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
async with agent.session() as session:
|
|
18
|
+
await session.run("Help me configure my project")
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Annotated, Literal
|
|
22
|
+
|
|
23
|
+
from pydantic import BaseModel, Field
|
|
24
|
+
from rich.panel import Panel
|
|
25
|
+
from rich.prompt import Confirm, Prompt
|
|
26
|
+
|
|
27
|
+
from stirrup.core.models import Tool, ToolResult, ToolUseCountMetadata
|
|
28
|
+
from stirrup.utils.logging import AgentLoggerBase, console
|
|
29
|
+
|
|
30
|
+
__all__ = ["USER_INPUT_TOOL", "UserInputParams"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UserInputParams(BaseModel):
|
|
34
|
+
"""Parameters for asking the user a single question.
|
|
35
|
+
|
|
36
|
+
Supports three question types:
|
|
37
|
+
- "text": Free-form text input (default)
|
|
38
|
+
- "choice": Multiple choice from a list of options
|
|
39
|
+
- "confirm": Yes/no confirmation
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
question: Annotated[str, Field(description="A single question to ask the user (*not* multiple questions)")]
|
|
43
|
+
question_type: Annotated[
|
|
44
|
+
Literal["text", "choice", "confirm"],
|
|
45
|
+
Field(
|
|
46
|
+
default="text",
|
|
47
|
+
description="Type of question: 'text' for free-form, 'choice' for multiple choice, 'confirm' for yes/no",
|
|
48
|
+
),
|
|
49
|
+
]
|
|
50
|
+
choices: Annotated[
|
|
51
|
+
list[str] | None,
|
|
52
|
+
Field(default=None, description="List of valid choices (required when question_type is 'choice')"),
|
|
53
|
+
]
|
|
54
|
+
default: Annotated[
|
|
55
|
+
str,
|
|
56
|
+
Field(default="", description="Default value if user presses Enter without input"),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_logger() -> "AgentLoggerBase | None":
|
|
61
|
+
"""Get the current session's logger for pause/resume.
|
|
62
|
+
|
|
63
|
+
Returns the logger from SessionState if available, None otherwise.
|
|
64
|
+
"""
|
|
65
|
+
from stirrup.core.agent import _SESSION_STATE
|
|
66
|
+
|
|
67
|
+
state = _SESSION_STATE.get(None)
|
|
68
|
+
return state.logger if state else None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def user_input_executor(params: UserInputParams) -> ToolResult[ToolUseCountMetadata]:
|
|
72
|
+
"""Prompt the user for input and return their response."""
|
|
73
|
+
logger = _get_logger()
|
|
74
|
+
|
|
75
|
+
# Pause spinner before prompting
|
|
76
|
+
if logger:
|
|
77
|
+
logger.pause_live()
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Print newline to separate from spinner, then display question in a styled panel
|
|
81
|
+
console.print()
|
|
82
|
+
panel = Panel(
|
|
83
|
+
params.question,
|
|
84
|
+
title="[bold cyan]🤔 Agent Question[/]",
|
|
85
|
+
title_align="left",
|
|
86
|
+
border_style="cyan",
|
|
87
|
+
padding=(0, 1),
|
|
88
|
+
)
|
|
89
|
+
console.print(panel)
|
|
90
|
+
|
|
91
|
+
# Get user input based on question type
|
|
92
|
+
if params.question_type == "confirm":
|
|
93
|
+
# Yes/no confirmation
|
|
94
|
+
default_bool = params.default.lower() in ("yes", "y", "true", "1") if params.default else False
|
|
95
|
+
result = Confirm.ask("[bold]Your answer[/]", default=default_bool, console=console)
|
|
96
|
+
answer = "yes" if result else "no"
|
|
97
|
+
|
|
98
|
+
elif params.question_type == "choice" and params.choices:
|
|
99
|
+
# Multiple choice
|
|
100
|
+
answer = Prompt.ask(
|
|
101
|
+
"[bold]Your answer[/]",
|
|
102
|
+
choices=params.choices,
|
|
103
|
+
default=params.default,
|
|
104
|
+
console=console,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
else:
|
|
108
|
+
# Free-form text (default)
|
|
109
|
+
answer = Prompt.ask("[bold]Your answer[/]", default=params.default or "", console=console)
|
|
110
|
+
|
|
111
|
+
return ToolResult(content=answer, metadata=ToolUseCountMetadata())
|
|
112
|
+
|
|
113
|
+
finally:
|
|
114
|
+
# Always resume spinner, even if an exception occurs
|
|
115
|
+
if logger:
|
|
116
|
+
logger.resume_live()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
USER_INPUT_TOOL: Tool[UserInputParams, ToolUseCountMetadata] = Tool(
|
|
120
|
+
name="user_input",
|
|
121
|
+
description=(
|
|
122
|
+
"Ask the user a question when you need clarification or are uncertain. "
|
|
123
|
+
"Supports three types: 'text' for free-form input, 'choice' for multiple choice "
|
|
124
|
+
"(provide choices list), 'confirm' for yes/no questions. Returns the user's response."
|
|
125
|
+
"There should only EVER be one question per call to this tool."
|
|
126
|
+
"If you need to ask multiple questions, you should call this tool multiple times."
|
|
127
|
+
),
|
|
128
|
+
parameters=UserInputParams,
|
|
129
|
+
executor=user_input_executor,
|
|
130
|
+
)
|
stirrup/tools/web.py
CHANGED
|
@@ -125,6 +125,7 @@ def _get_fetch_web_page_tool(client: httpx.AsyncClient | None = None) -> Tool[Fe
|
|
|
125
125
|
return ToolResult(
|
|
126
126
|
content=f"<web_fetch><url>{params.url}</url><error>"
|
|
127
127
|
f"{truncate_msg(str(exc), MAX_LENGTH_WEB_FETCH_HTML)}</error></web_fetch>",
|
|
128
|
+
success=False,
|
|
128
129
|
metadata=WebFetchMetadata(pages_fetched=[params.url]),
|
|
129
130
|
)
|
|
130
131
|
|
stirrup/utils/logging.py
CHANGED
|
@@ -23,10 +23,12 @@ from rich.text import Text
|
|
|
23
23
|
from rich.tree import Tree
|
|
24
24
|
|
|
25
25
|
from stirrup.core.models import AssistantMessage, ToolMessage, UserMessage, _aggregate_list, aggregate_metadata
|
|
26
|
+
from stirrup.utils.text import truncate_msg
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
28
29
|
"AgentLogger",
|
|
29
30
|
"AgentLoggerBase",
|
|
31
|
+
"console",
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
# Shared console instance
|
|
@@ -247,6 +249,12 @@ class AgentLoggerBase(ABC):
|
|
|
247
249
|
"""Log an error message."""
|
|
248
250
|
...
|
|
249
251
|
|
|
252
|
+
def pause_live(self) -> None: # noqa: B027
|
|
253
|
+
"""Pause live display (e.g., spinner) before user interaction."""
|
|
254
|
+
|
|
255
|
+
def resume_live(self) -> None: # noqa: B027
|
|
256
|
+
"""Resume live display after user interaction."""
|
|
257
|
+
|
|
250
258
|
|
|
251
259
|
class AgentLogger(AgentLoggerBase):
|
|
252
260
|
"""Rich console logger for agent workflows.
|
|
@@ -655,6 +663,23 @@ class AgentLogger(AgentLoggerBase):
|
|
|
655
663
|
if self._live:
|
|
656
664
|
self._live.update(self._make_spinner())
|
|
657
665
|
|
|
666
|
+
def pause_live(self) -> None:
|
|
667
|
+
"""Pause the live spinner display.
|
|
668
|
+
|
|
669
|
+
Call this before prompting for user input to prevent the spinner
|
|
670
|
+
from interfering with the input prompt.
|
|
671
|
+
"""
|
|
672
|
+
if self._live is not None:
|
|
673
|
+
self._live.stop()
|
|
674
|
+
|
|
675
|
+
def resume_live(self) -> None:
|
|
676
|
+
"""Resume the live spinner display.
|
|
677
|
+
|
|
678
|
+
Call this after user input is complete to restart the spinner.
|
|
679
|
+
"""
|
|
680
|
+
if self._live is not None:
|
|
681
|
+
self._live.start()
|
|
682
|
+
|
|
658
683
|
def set_level(self, level: int) -> None:
|
|
659
684
|
"""Set the logging level."""
|
|
660
685
|
self._level = level
|
|
@@ -745,11 +770,12 @@ class AgentLogger(AgentLoggerBase):
|
|
|
745
770
|
content.append("\n\n")
|
|
746
771
|
content.append("Tool Calls:\n", style="bold magenta")
|
|
747
772
|
for tc in assistant_message.tool_calls:
|
|
748
|
-
args_parsed = json.loads(tc.arguments)
|
|
749
|
-
args_formatted = json.dumps(args_parsed, indent=2, ensure_ascii=False)
|
|
750
|
-
args_preview = args_formatted[:1000] + "..." if len(args_formatted) > 1000 else args_formatted
|
|
751
773
|
content.append(f" 🔧 {tc.name}", style="magenta")
|
|
752
|
-
|
|
774
|
+
if tc.arguments and tc.arguments.strip():
|
|
775
|
+
args_parsed = json.loads(tc.arguments)
|
|
776
|
+
args_formatted = json.dumps(args_parsed, indent=2, ensure_ascii=False)
|
|
777
|
+
args_preview = args_formatted[:1000] + "..." if len(args_formatted) > 1000 else args_formatted
|
|
778
|
+
content.append(args_preview, style="dim")
|
|
753
779
|
|
|
754
780
|
# Create and print panel with agent name in title
|
|
755
781
|
title = f"[bold]AssistantMessage[/bold] │ {self.name} │ Turn {turn}/{max_turns}"
|
|
@@ -851,9 +877,8 @@ class AgentLogger(AgentLoggerBase):
|
|
|
851
877
|
# Unescape HTML entities (e.g., < -> <, > -> >, & -> &)
|
|
852
878
|
result_text = html.unescape(result_text)
|
|
853
879
|
|
|
854
|
-
# Truncate long results
|
|
855
|
-
|
|
856
|
-
result_text = result_text[:1000] + "..."
|
|
880
|
+
# Truncate long results (keeps start and end, removes middle)
|
|
881
|
+
result_text = truncate_msg(result_text, 1000)
|
|
857
882
|
|
|
858
883
|
# Format as XML with syntax highlighting
|
|
859
884
|
content = Syntax(result_text, "xml", theme="monokai", word_wrap=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: stirrup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: The lightweight foundation for building agents
|
|
5
5
|
Keywords: ai,agent,llm,openai,anthropic,tools,framework
|
|
6
6
|
Author: Artificial Analysis, Inc.
|
|
@@ -47,17 +47,19 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
47
47
|
Requires-Dist: rich>=13.0.0
|
|
48
48
|
Requires-Dist: tenacity>=5.0.0
|
|
49
49
|
Requires-Dist: trafilatura>=1.9.0
|
|
50
|
-
Requires-Dist: stirrup[litellm,e2b,docker,mcp] ; extra == 'all'
|
|
50
|
+
Requires-Dist: stirrup[litellm,e2b,docker,mcp,browser] ; extra == 'all'
|
|
51
|
+
Requires-Dist: browser-use>=0.11.3 ; extra == 'browser'
|
|
51
52
|
Requires-Dist: docker>=7.0.0 ; extra == 'docker'
|
|
52
53
|
Requires-Dist: python-dotenv>=1.0.0 ; extra == 'docker'
|
|
53
54
|
Requires-Dist: e2b-code-interpreter>=2.3.0 ; extra == 'e2b'
|
|
54
55
|
Requires-Dist: litellm>=1.79.3 ; extra == 'litellm'
|
|
55
56
|
Requires-Dist: mcp>=1.9.0 ; extra == 'mcp'
|
|
56
57
|
Requires-Python: >=3.12
|
|
57
|
-
Project-URL: Documentation, https://stirrup.artificialanalysis.ai
|
|
58
58
|
Project-URL: Homepage, https://github.com/ArtificialAnalysis/Stirrup
|
|
59
|
+
Project-URL: Documentation, https://stirrup.artificialanalysis.ai
|
|
59
60
|
Project-URL: Repository, https://github.com/ArtificialAnalysis/Stirrup
|
|
60
61
|
Provides-Extra: all
|
|
62
|
+
Provides-Extra: browser
|
|
61
63
|
Provides-Extra: docker
|
|
62
64
|
Provides-Extra: e2b
|
|
63
65
|
Provides-Extra: litellm
|
|
@@ -91,16 +93,16 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
91
93
|
|
|
92
94
|
## Features
|
|
93
95
|
|
|
94
|
-
- **
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
- **
|
|
100
|
-
- **
|
|
101
|
-
- **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
102
|
-
- **Flexible provider support:** Pre-built support for OpenAI-compatible APIs
|
|
103
|
-
- **Multimodal support:** Process images, video, and audio with automatic format conversion
|
|
96
|
+
- 🧪 **Code execution:** Run code locally, in Docker, or in an E2B sandbox
|
|
97
|
+
- 🔎 **Online search / web browsing:** Search and fetch web pages
|
|
98
|
+
- 🔌 **MCP client support:** Connect to MCP servers and use their tools/resources
|
|
99
|
+
- 📄 **Document input and output:** Import files into context and produce file outputs
|
|
100
|
+
- 🧩 **Skills system:** Extend agents with modular, domain-specific instruction packages
|
|
101
|
+
- 🛠️ **Flexible tool execution:** A generic `Tool` interface allows easy tool definition
|
|
102
|
+
- 👤 **Human-in-the-loop:** Includes a built-in user input tool that enables human feedback or clarification during agent execution
|
|
103
|
+
- 🧠 **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
104
|
+
- 🔁 **Flexible provider support:** Pre-built support for OpenAI-compatible APIs, LiteLLM, or bring your own client
|
|
105
|
+
- 🖼️ **Multimodal support:** Process images, video, and audio with automatic format conversion
|
|
104
106
|
|
|
105
107
|
## Installation
|
|
106
108
|
|
|
@@ -116,6 +118,7 @@ pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
|
116
118
|
pip install 'stirrup[docker]' # or: uv add 'stirrup[docker]'
|
|
117
119
|
pip install 'stirrup[e2b]' # or: uv add 'stirrup[e2b]'
|
|
118
120
|
pip install 'stirrup[mcp]' # or: uv add 'stirrup[mcp]'
|
|
121
|
+
pip install 'stirrup[browser]' # or: uv add 'stirrup[browser]'
|
|
119
122
|
```
|
|
120
123
|
|
|
121
124
|
## Quick Start
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
stirrup/__init__.py,sha256=4p5Rw7f_wdxVu1FJTgJROe0aTlnc8tOsainBEzDRGEY,1905
|
|
2
|
+
stirrup/clients/__init__.py,sha256=vHtR1rqK7w9LMWCBJXJde_R1pwdbuj9f6AbAJ4ib3xk,622
|
|
3
|
+
stirrup/clients/chat_completions_client.py,sha256=P0VOGFQcfLIVf7zCGYbopQDjNVEJlpeLBeGvB87sQQg,7390
|
|
4
|
+
stirrup/clients/litellm_client.py,sha256=2ZrZKKAEV2dEFs6ze4qBl3it5VwiGH8s7wHVuHdw-uY,5507
|
|
5
|
+
stirrup/clients/open_responses_client.py,sha256=AEM2z_LZhT4hW7VUSFb4mqzV5q_mavnLCwiBTUmt340,15303
|
|
6
|
+
stirrup/clients/utils.py,sha256=Z_8KiENDZVD9fChUm7PA-RLhvoChswHVQsjrHXlIfkg,5684
|
|
7
|
+
stirrup/constants.py,sha256=WpVPm2jRN2AqYMyoMYeJimiggoquP7M3IrcHNpduFF4,644
|
|
8
|
+
stirrup/core/__init__.py,sha256=ReBVl7B9h_FNkZ77vCx2xlfuK1JuQ0yTSXrEgc4tONU,39
|
|
9
|
+
stirrup/core/agent.py,sha256=I-SDjXxUOYSVxowy7SHXP_bjEEZD2JIouNnS7VarmxI,58120
|
|
10
|
+
stirrup/core/cache.py,sha256=lAbbBJzgYInewoBBPMzNooroU2Q7JbF21riggfdIDa8,16697
|
|
11
|
+
stirrup/core/exceptions.py,sha256=CzLVAi7Ns-t9BWSkqQUCB7ypVHAesV2s4a09-i0NXyQ,213
|
|
12
|
+
stirrup/core/models.py,sha256=hAwquChulASzwf0yirmsWkfgBEnLTxX9pTAkJ7CwVvM,22591
|
|
13
|
+
stirrup/prompts/__init__.py,sha256=e4bpTktBaFPuO_bIW5DelGNWtT6_NIUqnD2lRv8n89I,796
|
|
14
|
+
stirrup/prompts/base_system_prompt.txt,sha256=D_UlDWEnG2yaPCMFrE7IqMHI8VCzi4BZ-GnuL3qs5q4,288
|
|
15
|
+
stirrup/prompts/message_summarizer.txt,sha256=uQoTxreMuC42rTGSZmoH1Dnj06WrEQb0gLkDvVMhosQ,1173
|
|
16
|
+
stirrup/prompts/message_summarizer_bridge.txt,sha256=sWbfnHtI6RWemBIyQsnqHMGpnU-E6FTbfUC6rvkEHLY,372
|
|
17
|
+
stirrup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
stirrup/skills/__init__.py,sha256=BEcmdSskfBzx_QK4eKXECucndIKRjHXzzwwwsaez8k4,700
|
|
19
|
+
stirrup/skills/skills.py,sha256=qhA3HI55kaRqLyvn_56Cs71833Xacg-8qP7muHrwruE,4282
|
|
20
|
+
stirrup/tools/__init__.py,sha256=TJ3AqPhAkfVPboyMGHscIfq-oUc8brPPVnKtUrf9ICU,2879
|
|
21
|
+
stirrup/tools/browser_use.py,sha256=m7y5F9mO_Qhitmpu6NFh9-wYAxqgXb4_T3VuZdgGInM,21111
|
|
22
|
+
stirrup/tools/calculator.py,sha256=Cckt-8TtltxtuyY_Hh0wOr8efUzBZzg7rG4dBpvpuRM,1293
|
|
23
|
+
stirrup/tools/code_backends/__init__.py,sha256=O3Rs76r0YcQ27voTrx_zuhIEFawK3b1TQdKi70MORG8,987
|
|
24
|
+
stirrup/tools/code_backends/base.py,sha256=aOX09b4sCxjtFecnnklE_dINIfQ_lH3oYZJ3-N7rCMA,17720
|
|
25
|
+
stirrup/tools/code_backends/docker.py,sha256=6zeOL40peNGu3XGEzP6iwVIZmpr2TjENkllt5DZjHr4,30884
|
|
26
|
+
stirrup/tools/code_backends/e2b.py,sha256=hhaDVOYKxbYHIUhFw5kv-Pri0Sb3a-Weh5Zw5H0tyKM,15396
|
|
27
|
+
stirrup/tools/code_backends/local.py,sha256=6D6QnGYyspQbJ5HOaDke-asGc0-W55p-Nh5ABQkzjG0,20125
|
|
28
|
+
stirrup/tools/finish.py,sha256=xprl0z1N_e-8VddLyEIA9pLeMXjh5A9d4LpkKxhOh5A,1803
|
|
29
|
+
stirrup/tools/mcp.py,sha256=4wWYae95y8Bs7e36hHwnxRfVVj0PABrsRStw492lLaw,18749
|
|
30
|
+
stirrup/tools/user_input.py,sha256=XwK14FvRQly3vGwgNzPVGoSXfbco0WWaSVpTDyjV09E,4508
|
|
31
|
+
stirrup/tools/view_image.py,sha256=zazCpZMtLOD6lplLPYGNQ8JeYfc0oUDJoUUyVAp3AMU,3126
|
|
32
|
+
stirrup/tools/web.py,sha256=B-zp5i1WhjOOMAlYtnvU3N5hNYnJYm8qVXAtNx_ZaRw,12292
|
|
33
|
+
stirrup/utils/__init__.py,sha256=4kcuExrphSXqgxRgu1q8_Z6Rrb9aAZpIo4Xq4S9Twuk,230
|
|
34
|
+
stirrup/utils/logging.py,sha256=vZ7P7MotZnjbTyhMZ0p1YVTGhKQBAXBHOGbku3gEzuk,35166
|
|
35
|
+
stirrup/utils/text.py,sha256=3lGlcXFzQ-Mclsbu7wJciG3CcHvQ_Sk98tqOZxYLlGw,479
|
|
36
|
+
stirrup-0.1.4.dist-info/WHEEL,sha256=XV0cjMrO7zXhVAIyyc8aFf1VjZ33Fen4IiJk5zFlC3g,80
|
|
37
|
+
stirrup-0.1.4.dist-info/METADATA,sha256=tJvHDTGB-Tfh4YqJVzyny9GAHqd6aZdqhpYG5apy3p8,13304
|
|
38
|
+
stirrup-0.1.4.dist-info/RECORD,,
|
stirrup-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
stirrup/__init__.py,sha256=Dol0Uui-gBslpplfEUrgxkqqJWr1ucZJej5finNvCeI,1869
|
|
2
|
-
stirrup/clients/__init__.py,sha256=oO8VMHmbUhoxFyC0JLQs_kUFNSRlvTj5xz0FgzBb98E,405
|
|
3
|
-
stirrup/clients/chat_completions_client.py,sha256=p_EeXqRuo6mWnzgMhy22SWdHE6_OXIRyABvCKgHdnu4,7586
|
|
4
|
-
stirrup/clients/litellm_client.py,sha256=J-HDv7ZZTkNYC-aeSNyd7xTDd_5r8DEeXOPz9eQMC7A,4985
|
|
5
|
-
stirrup/clients/utils.py,sha256=Yyeh6unQSvqgDTDhjpD5DoRu_wP_nWfsNv9DGXQwgo8,5452
|
|
6
|
-
stirrup/constants.py,sha256=h3NzsePJ4FKpImTpV5xtFeJarKb67jR_6n89tNOkQYs,523
|
|
7
|
-
stirrup/core/__init__.py,sha256=ReBVl7B9h_FNkZ77vCx2xlfuK1JuQ0yTSXrEgc4tONU,39
|
|
8
|
-
stirrup/core/agent.py,sha256=tt1V564B6n_C0ffyH24LuC6PhE4EJA8NOolQCxDG9iw,50836
|
|
9
|
-
stirrup/core/exceptions.py,sha256=CzLVAi7Ns-t9BWSkqQUCB7ypVHAesV2s4a09-i0NXyQ,213
|
|
10
|
-
stirrup/core/models.py,sha256=KAyjJIoqbhgy_MN0sPwGI8XWxSiziPnyndMD05ylj_U,21121
|
|
11
|
-
stirrup/prompts/__init__.py,sha256=e4bpTktBaFPuO_bIW5DelGNWtT6_NIUqnD2lRv8n89I,796
|
|
12
|
-
stirrup/prompts/base_system_prompt.txt,sha256=KZ2_JhJ91u4oMqRZvhuAp99nb6ZkXkJdVbIRN6drVME,348
|
|
13
|
-
stirrup/prompts/message_summarizer.txt,sha256=uQoTxreMuC42rTGSZmoH1Dnj06WrEQb0gLkDvVMhosQ,1173
|
|
14
|
-
stirrup/prompts/message_summarizer_bridge.txt,sha256=sWbfnHtI6RWemBIyQsnqHMGpnU-E6FTbfUC6rvkEHLY,372
|
|
15
|
-
stirrup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
stirrup/skills/__init__.py,sha256=BEcmdSskfBzx_QK4eKXECucndIKRjHXzzwwwsaez8k4,700
|
|
17
|
-
stirrup/skills/skills.py,sha256=qhA3HI55kaRqLyvn_56Cs71833Xacg-8qP7muHrwruE,4282
|
|
18
|
-
stirrup/tools/__init__.py,sha256=ohyeMvXb6oURiAyoHi0VmC9ksZSRyGleT341VNzHCy4,2714
|
|
19
|
-
stirrup/tools/calculator.py,sha256=JkuGmGZJtaKbC4vHVrIph4aTjlGcFMhhv5MB1ntqgv4,1278
|
|
20
|
-
stirrup/tools/code_backends/__init__.py,sha256=O3Rs76r0YcQ27voTrx_zuhIEFawK3b1TQdKi70MORG8,987
|
|
21
|
-
stirrup/tools/code_backends/base.py,sha256=Nx0tTDX4GKoBWQK2F953vSsFgWCcOd_1WNtYCA4FG4o,17021
|
|
22
|
-
stirrup/tools/code_backends/docker.py,sha256=Xx4aBZ1uXVznP0qV4tXL2PMMs-8QEPw1bIPvgPasEGk,30281
|
|
23
|
-
stirrup/tools/code_backends/e2b.py,sha256=7wV1SOu4S5g5uCtnipC1xNg8kBzCrudyIEOIkf-JCkE,14072
|
|
24
|
-
stirrup/tools/code_backends/local.py,sha256=WV-MMcPY5ooKPhOwd3JUUz718Ht8eRyYklGAZ0gkrx4,19598
|
|
25
|
-
stirrup/tools/finish.py,sha256=K_NxwOwdvncT2QTua2A_8lZ9MwK4WQQ5FL2gdUrE29c,936
|
|
26
|
-
stirrup/tools/mcp.py,sha256=4wWYae95y8Bs7e36hHwnxRfVVj0PABrsRStw492lLaw,18749
|
|
27
|
-
stirrup/tools/view_image.py,sha256=zazCpZMtLOD6lplLPYGNQ8JeYfc0oUDJoUUyVAp3AMU,3126
|
|
28
|
-
stirrup/tools/web.py,sha256=2yfBJsu8GgFI7Oh1dFlXwNaXth6WQfGpbJFU-rV-yuI,12261
|
|
29
|
-
stirrup/utils/__init__.py,sha256=4kcuExrphSXqgxRgu1q8_Z6Rrb9aAZpIo4Xq4S9Twuk,230
|
|
30
|
-
stirrup/utils/logging.py,sha256=3Li6MhjLJWwaXHDZ06EjgW42XDL6V3ZDt9rccV_ZYZ4,34292
|
|
31
|
-
stirrup/utils/text.py,sha256=3lGlcXFzQ-Mclsbu7wJciG3CcHvQ_Sk98tqOZxYLlGw,479
|
|
32
|
-
stirrup-0.1.2.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
33
|
-
stirrup-0.1.2.dist-info/METADATA,sha256=eIK1F1yXhCgFspDO8q5J48B7P3Jt16QZez3ZBVFU5n8,12862
|
|
34
|
-
stirrup-0.1.2.dist-info/RECORD,,
|