stirrup 0.1.0__py3-none-any.whl → 0.1.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.
- stirrup/core/agent.py +26 -0
- stirrup/core/models.py +4 -0
- stirrup/skills/__init__.py +24 -0
- stirrup/skills/skills.py +145 -0
- stirrup/tools/code_backends/docker.py +16 -4
- stirrup/tools/code_backends/e2b.py +7 -2
- stirrup/tools/code_backends/local.py +14 -2
- {stirrup-0.1.0.dist-info → stirrup-0.1.2.dist-info}/METADATA +38 -17
- {stirrup-0.1.0.dist-info → stirrup-0.1.2.dist-info}/RECORD +10 -8
- {stirrup-0.1.0.dist-info → stirrup-0.1.2.dist-info}/WHEEL +1 -1
stirrup/core/agent.py
CHANGED
|
@@ -36,6 +36,7 @@ from stirrup.core.models import (
|
|
|
36
36
|
UserMessage,
|
|
37
37
|
)
|
|
38
38
|
from stirrup.prompts import MESSAGE_SUMMARIZER, MESSAGE_SUMMARIZER_BRIDGE_TEMPLATE
|
|
39
|
+
from stirrup.skills import SkillMetadata, format_skills_section, load_skills_metadata
|
|
39
40
|
from stirrup.tools import DEFAULT_TOOLS
|
|
40
41
|
from stirrup.tools.code_backends.base import CodeExecToolProvider
|
|
41
42
|
from stirrup.tools.code_backends.local import LocalCodeExecToolProvider
|
|
@@ -70,6 +71,7 @@ class SessionState:
|
|
|
70
71
|
parent_exec_env: CodeExecToolProvider | None = None
|
|
71
72
|
depth: int = 0
|
|
72
73
|
uploaded_file_paths: list[str] = field(default_factory=list) # Paths of files uploaded to exec_env
|
|
74
|
+
skills_metadata: list[SkillMetadata] = field(default_factory=list) # Loaded skills metadata
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
_SESSION_STATE: contextvars.ContextVar[SessionState] = contextvars.ContextVar("session_state")
|
|
@@ -222,6 +224,7 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
222
224
|
# Session configuration (set during session(), used in __aenter__)
|
|
223
225
|
self._pending_output_dir: Path | None = None
|
|
224
226
|
self._pending_input_files: str | Path | list[str | Path] | None = None
|
|
227
|
+
self._pending_skills_dir: Path | None = None
|
|
225
228
|
|
|
226
229
|
# Instance-scoped state (populated during __aenter__, isolated per agent instance)
|
|
227
230
|
self._active_tools: dict[str, Tool] = {}
|
|
@@ -258,6 +261,7 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
258
261
|
self,
|
|
259
262
|
output_dir: Path | str | None = None,
|
|
260
263
|
input_files: str | Path | list[str | Path] | None = None,
|
|
264
|
+
skills_dir: Path | str | None = None,
|
|
261
265
|
) -> Self:
|
|
262
266
|
"""Configure a session and return self for use as async context manager.
|
|
263
267
|
|
|
@@ -270,6 +274,9 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
270
274
|
- Glob patterns (e.g., "data/*.csv", "**/*.py")
|
|
271
275
|
Raises ValueError if no CodeExecToolProvider is configured
|
|
272
276
|
or if a glob pattern matches no files.
|
|
277
|
+
skills_dir: Directory containing skill definitions to load and make available
|
|
278
|
+
to the agent. Skills are uploaded to the execution environment
|
|
279
|
+
and their metadata is included in the system prompt.
|
|
273
280
|
|
|
274
281
|
Returns:
|
|
275
282
|
Self, for use with `async with agent.session(...) as session:`
|
|
@@ -285,6 +292,7 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
285
292
|
"""
|
|
286
293
|
self._pending_output_dir = Path(output_dir) if output_dir else None
|
|
287
294
|
self._pending_input_files = input_files
|
|
295
|
+
self._pending_skills_dir = Path(skills_dir) if skills_dir else None
|
|
288
296
|
return self
|
|
289
297
|
|
|
290
298
|
def _resolve_input_files(self, input_files: str | Path | list[str | Path]) -> list[Path]:
|
|
@@ -410,6 +418,12 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
410
418
|
files_section += f"\n- {file_path}"
|
|
411
419
|
parts.append(files_section)
|
|
412
420
|
|
|
421
|
+
# Skills section (if skills were loaded)
|
|
422
|
+
if state and state.skills_metadata:
|
|
423
|
+
skills_section = format_skills_section(state.skills_metadata)
|
|
424
|
+
if skills_section:
|
|
425
|
+
parts.append(f"\n\n{skills_section}")
|
|
426
|
+
|
|
413
427
|
# User's custom system prompt (if provided)
|
|
414
428
|
if self._system_prompt:
|
|
415
429
|
parts.append(f"\n\nFollow these instructions from the User:\n{self._system_prompt}")
|
|
@@ -588,6 +602,18 @@ class Agent[FinishParams: BaseModel, FinishMeta]:
|
|
|
588
602
|
raise RuntimeError(f"Failed to upload files: {result.failed}")
|
|
589
603
|
self._pending_input_files = None # Clear pending state
|
|
590
604
|
|
|
605
|
+
# Upload skills directory if it exists and load metadata
|
|
606
|
+
if self._pending_skills_dir:
|
|
607
|
+
skills_path = self._pending_skills_dir
|
|
608
|
+
if skills_path.exists() and skills_path.is_dir():
|
|
609
|
+
if state.exec_env:
|
|
610
|
+
logger.debug("[%s __aenter__] Uploading skills directory: %s", self._name, skills_path)
|
|
611
|
+
await state.exec_env.upload_files(skills_path, dest_dir="skills")
|
|
612
|
+
# Load skills metadata (even if no exec_env, for system prompt)
|
|
613
|
+
state.skills_metadata = load_skills_metadata(skills_path)
|
|
614
|
+
logger.debug("[%s __aenter__] Loaded %d skills", self._name, len(state.skills_metadata))
|
|
615
|
+
self._pending_skills_dir = None # Clear pending state
|
|
616
|
+
|
|
591
617
|
# Configure and enter logger context
|
|
592
618
|
self._logger.name = self._name
|
|
593
619
|
self._logger.model = self._client.model_slug
|
stirrup/core/models.py
CHANGED
|
@@ -430,6 +430,7 @@ class Tool[P: BaseModel, M](BaseModel):
|
|
|
430
430
|
(setup/teardown, resource pooling), use a ToolProvider instead.
|
|
431
431
|
|
|
432
432
|
Example with parameters:
|
|
433
|
+
```python
|
|
433
434
|
class CalcParams(BaseModel):
|
|
434
435
|
expression: str
|
|
435
436
|
|
|
@@ -439,13 +440,16 @@ class Tool[P: BaseModel, M](BaseModel):
|
|
|
439
440
|
parameters=CalcParams,
|
|
440
441
|
executor=lambda p: ToolResult(content=str(eval(p.expression))),
|
|
441
442
|
)
|
|
443
|
+
```
|
|
442
444
|
|
|
443
445
|
Example without parameters:
|
|
446
|
+
```python
|
|
444
447
|
time_tool = Tool[None, None](
|
|
445
448
|
name="time",
|
|
446
449
|
description="Get current time",
|
|
447
450
|
executor=lambda _: ToolResult(content=datetime.now().isoformat()),
|
|
448
451
|
)
|
|
452
|
+
```
|
|
449
453
|
"""
|
|
450
454
|
|
|
451
455
|
name: str
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Skills module for agent capabilities.
|
|
2
|
+
|
|
3
|
+
This module provides functionality for loading and managing agent skills.
|
|
4
|
+
Skills are modular packages with instructions and resources that agents
|
|
5
|
+
can discover and use dynamically.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
from stirrup.skills import load_skills_metadata, format_skills_section
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
# Load skills from directory
|
|
12
|
+
skills = load_skills_metadata(Path("skills"))
|
|
13
|
+
|
|
14
|
+
# Format for system prompt
|
|
15
|
+
prompt_section = format_skills_section(skills)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from stirrup.skills.skills import SkillMetadata, format_skills_section, load_skills_metadata
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"SkillMetadata",
|
|
22
|
+
"format_skills_section",
|
|
23
|
+
"load_skills_metadata",
|
|
24
|
+
]
|
stirrup/skills/skills.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Skills loader for agent capabilities.
|
|
2
|
+
|
|
3
|
+
Skills are modular packages that extend agent capabilities with instructions,
|
|
4
|
+
scripts, and resources. Each skill is a directory containing a SKILL.md file
|
|
5
|
+
with YAML frontmatter (name, description) and detailed instructions.
|
|
6
|
+
|
|
7
|
+
Based on: https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SkillMetadata:
|
|
20
|
+
"""Metadata extracted from a skill's SKILL.md frontmatter."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
description: str
|
|
24
|
+
path: str # Relative path like "skills/data_analysis"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_frontmatter(content: str) -> dict[str, str]:
|
|
28
|
+
"""Parse YAML frontmatter from markdown content.
|
|
29
|
+
|
|
30
|
+
Extracts metadata between --- markers at the start of the file.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
content: Markdown content with optional YAML frontmatter
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Dictionary of frontmatter key-value pairs, empty if no frontmatter found
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
# Match YAML frontmatter between --- markers
|
|
40
|
+
pattern = r"^---\s*\n(.*?)\n---"
|
|
41
|
+
match = re.match(pattern, content, re.DOTALL)
|
|
42
|
+
|
|
43
|
+
if not match:
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
frontmatter_text = match.group(1)
|
|
47
|
+
result: dict[str, str] = {}
|
|
48
|
+
|
|
49
|
+
# Simple YAML parsing for key: value pairs
|
|
50
|
+
for line in frontmatter_text.strip().split("\n"):
|
|
51
|
+
line = line.strip()
|
|
52
|
+
if ":" in line:
|
|
53
|
+
key, value = line.split(":", 1)
|
|
54
|
+
result[key.strip()] = value.strip()
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_skills_metadata(skills_dir: Path) -> list[SkillMetadata]:
|
|
60
|
+
"""Scan skills directory for SKILL.md files and extract metadata.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
skills_dir: Path to the skills directory
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of SkillMetadata for each valid skill found.
|
|
67
|
+
Returns empty list if skills_dir doesn't exist or has no skills.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
if not skills_dir.exists():
|
|
71
|
+
logger.debug("Skills directory does not exist: %s", skills_dir)
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
if not skills_dir.is_dir():
|
|
75
|
+
logger.warning("Skills path is not a directory: %s", skills_dir)
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
skills: list[SkillMetadata] = []
|
|
79
|
+
|
|
80
|
+
for skill_path in skills_dir.iterdir():
|
|
81
|
+
if not skill_path.is_dir():
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
skill_md = skill_path / "SKILL.md"
|
|
85
|
+
if not skill_md.exists():
|
|
86
|
+
logger.debug("Skill directory missing SKILL.md: %s", skill_path)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
content = skill_md.read_text(encoding="utf-8")
|
|
91
|
+
metadata = parse_frontmatter(content)
|
|
92
|
+
|
|
93
|
+
name = metadata.get("name")
|
|
94
|
+
description = metadata.get("description")
|
|
95
|
+
|
|
96
|
+
if not name or not description:
|
|
97
|
+
logger.warning(
|
|
98
|
+
"Skill %s missing required frontmatter (name, description)",
|
|
99
|
+
skill_path.name,
|
|
100
|
+
)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
skills.append(
|
|
104
|
+
SkillMetadata(
|
|
105
|
+
name=name,
|
|
106
|
+
description=description,
|
|
107
|
+
path=f"skills/{skill_path.name}",
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
logger.debug("Loaded skill: %s", name)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning("Failed to load skill %s: %s", skill_path.name, e)
|
|
114
|
+
|
|
115
|
+
return skills
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_skills_section(skills: list[SkillMetadata]) -> str:
|
|
119
|
+
"""Format skills metadata as a system prompt section.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
skills: List of skill metadata to include
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Formatted string for inclusion in system prompt.
|
|
126
|
+
Returns empty string if no skills provided.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
if not skills:
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
lines = [
|
|
133
|
+
"## Available Skills",
|
|
134
|
+
"",
|
|
135
|
+
"You have access to the following skills located in the `skills/` directory. "
|
|
136
|
+
"Each skill contains a SKILL.md file with detailed instructions and potentially bundled scripts.",
|
|
137
|
+
"",
|
|
138
|
+
"To use a skill:",
|
|
139
|
+
"1. Read the full instructions: `cat <skill_path>/SKILL.md`",
|
|
140
|
+
"2. Follow the instructions and use any bundled resources as described",
|
|
141
|
+
"",
|
|
142
|
+
]
|
|
143
|
+
lines.extend([f"- **{skill.name}**: {skill.description} (`{skill.path}/SKILL.md`)" for skill in skills])
|
|
144
|
+
|
|
145
|
+
return "\n".join(lines)
|
|
@@ -330,7 +330,7 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
330
330
|
context_path = self._dockerfile_context.resolve() if self._dockerfile_context else dockerfile_path.parent
|
|
331
331
|
|
|
332
332
|
# Generate unique tag based on dockerfile path
|
|
333
|
-
tag = f"
|
|
333
|
+
tag = f"stirrup-exec-env-{hashlib.md5(str(dockerfile_path).encode()).hexdigest()[:8]}"
|
|
334
334
|
|
|
335
335
|
logger.info("Building image from %s with tag %s", dockerfile_path, tag)
|
|
336
336
|
|
|
@@ -710,8 +710,20 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
710
710
|
logger.debug("Uploaded file: %s -> %s", source, container_path)
|
|
711
711
|
|
|
712
712
|
elif source.is_dir():
|
|
713
|
-
|
|
714
|
-
|
|
713
|
+
# If dest_dir was explicitly provided, copy contents directly to dest_base
|
|
714
|
+
# Otherwise, create a subdirectory with the source's name
|
|
715
|
+
if dest_dir:
|
|
716
|
+
dest = dest_base
|
|
717
|
+
# Copy contents of source directory into dest_base
|
|
718
|
+
for item in source.iterdir():
|
|
719
|
+
item_dest = dest / item.name
|
|
720
|
+
if item.is_file():
|
|
721
|
+
shutil.copy2(item, item_dest)
|
|
722
|
+
else:
|
|
723
|
+
shutil.copytree(item, item_dest, dirs_exist_ok=True)
|
|
724
|
+
else:
|
|
725
|
+
dest = dest_base / source.name
|
|
726
|
+
shutil.copytree(source, dest, dirs_exist_ok=True)
|
|
715
727
|
# Track all individual files uploaded
|
|
716
728
|
for file_path in source.rglob("*"):
|
|
717
729
|
if file_path.is_file():
|
|
@@ -725,7 +737,7 @@ class DockerCodeExecToolProvider(CodeExecToolProvider):
|
|
|
725
737
|
size=file_path.stat().st_size,
|
|
726
738
|
),
|
|
727
739
|
)
|
|
728
|
-
logger.debug("Uploaded directory: %s -> %s
|
|
740
|
+
logger.debug("Uploaded directory: %s -> %s", source, dest)
|
|
729
741
|
|
|
730
742
|
except Exception as exc:
|
|
731
743
|
result.failed[str(source)] = str(exc)
|
|
@@ -320,10 +320,12 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
320
320
|
|
|
321
321
|
elif source.is_dir():
|
|
322
322
|
# Upload all files in directory recursively
|
|
323
|
+
# If dest_dir was explicitly provided, copy contents directly to dest_base
|
|
324
|
+
# Otherwise, create a subdirectory with the source's name
|
|
323
325
|
for file_path in source.rglob("*"):
|
|
324
326
|
if file_path.is_file():
|
|
325
327
|
relative = file_path.relative_to(source)
|
|
326
|
-
dest = f"{dest_base}/{source.name}/{relative}"
|
|
328
|
+
dest = f"{dest_base}/{relative}" if dest_dir else f"{dest_base}/{source.name}/{relative}"
|
|
327
329
|
content = file_path.read_bytes()
|
|
328
330
|
await self._sbx.files.write(dest, content)
|
|
329
331
|
result.uploaded.append(
|
|
@@ -333,7 +335,10 @@ class E2BCodeExecToolProvider(CodeExecToolProvider):
|
|
|
333
335
|
size=len(content),
|
|
334
336
|
),
|
|
335
337
|
)
|
|
336
|
-
|
|
338
|
+
if dest_dir:
|
|
339
|
+
logger.debug("Uploaded directory contents: %s -> %s", source, dest_base)
|
|
340
|
+
else:
|
|
341
|
+
logger.debug("Uploaded directory: %s -> %s/%s", source, dest_base, source.name)
|
|
337
342
|
|
|
338
343
|
except Exception as exc:
|
|
339
344
|
result.failed[str(source)] = str(exc)
|
|
@@ -440,8 +440,20 @@ class LocalCodeExecToolProvider(CodeExecToolProvider):
|
|
|
440
440
|
logger.debug("Uploaded file: %s -> %s", source, dest)
|
|
441
441
|
|
|
442
442
|
elif source.is_dir():
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
# If dest_dir was explicitly provided, copy contents directly to dest_base
|
|
444
|
+
# Otherwise, create a subdirectory with the source's name
|
|
445
|
+
if dest_dir:
|
|
446
|
+
dest = dest_base
|
|
447
|
+
# Copy contents of source directory into dest_base
|
|
448
|
+
for item in source.iterdir():
|
|
449
|
+
item_dest = dest / item.name
|
|
450
|
+
if item.is_file():
|
|
451
|
+
shutil.copy2(item, item_dest)
|
|
452
|
+
else:
|
|
453
|
+
shutil.copytree(item, item_dest, dirs_exist_ok=True)
|
|
454
|
+
else:
|
|
455
|
+
dest = dest_base / source.name
|
|
456
|
+
shutil.copytree(source, dest, dirs_exist_ok=True)
|
|
445
457
|
# Track all individual files uploaded
|
|
446
458
|
for file_path in source.rglob("*"):
|
|
447
459
|
if file_path.is_file():
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: stirrup
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
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.
|
|
@@ -44,6 +44,7 @@ Requires-Dist: moviepy>=2.0.0
|
|
|
44
44
|
Requires-Dist: openai>=1.0.0
|
|
45
45
|
Requires-Dist: pillow>=10.4.0
|
|
46
46
|
Requires-Dist: pydantic>=2.0.0
|
|
47
|
+
Requires-Dist: rich>=13.0.0
|
|
47
48
|
Requires-Dist: tenacity>=5.0.0
|
|
48
49
|
Requires-Dist: trafilatura>=1.9.0
|
|
49
50
|
Requires-Dist: stirrup[litellm,e2b,docker,mcp] ; extra == 'all'
|
|
@@ -53,7 +54,7 @@ Requires-Dist: e2b-code-interpreter>=2.3.0 ; extra == 'e2b'
|
|
|
53
54
|
Requires-Dist: litellm>=1.79.3 ; extra == 'litellm'
|
|
54
55
|
Requires-Dist: mcp>=1.9.0 ; extra == 'mcp'
|
|
55
56
|
Requires-Python: >=3.12
|
|
56
|
-
Project-URL: Documentation, https://artificialanalysis.
|
|
57
|
+
Project-URL: Documentation, https://stirrup.artificialanalysis.ai
|
|
57
58
|
Project-URL: Homepage, https://github.com/ArtificialAnalysis/Stirrup
|
|
58
59
|
Project-URL: Repository, https://github.com/ArtificialAnalysis/Stirrup
|
|
59
60
|
Provides-Extra: all
|
|
@@ -64,9 +65,9 @@ Provides-Extra: mcp
|
|
|
64
65
|
Description-Content-Type: text/markdown
|
|
65
66
|
|
|
66
67
|
<div align="center">
|
|
67
|
-
<a href="">
|
|
68
|
+
<a href="https://stirrup.artificialanalysis.ai">
|
|
68
69
|
<picture>
|
|
69
|
-
<img alt="Stirrup" src="assets/stirrup-banner.png" width="700">
|
|
70
|
+
<img alt="Stirrup" src="https://github.com/ArtificialAnalysis/Stirrup/blob/048653717d8662b0b81d152a037995af1c926afc/assets/stirrup-banner.png?raw=true" width="700">
|
|
70
71
|
</picture>
|
|
71
72
|
</a>
|
|
72
73
|
<br></br>
|
|
@@ -78,7 +79,7 @@ Description-Content-Type: text/markdown
|
|
|
78
79
|
<p align="center">
|
|
79
80
|
<a href="https://pypi.python.org/pypi/stirrup"><img src="https://img.shields.io/pypi/v/stirrup" alt="PyPI version" /></a> <!--
|
|
80
81
|
--><a href="https://github.com/ArtificialAnalysis/Stirrup/blob/main/LICENSE"><img src="https://img.shields.io/github/license/ArtificialAnalysis/Stirrup" alt="License" /></a> <!--
|
|
81
|
-
--><a href="https://artificialanalysis.
|
|
82
|
+
--><a href="https://stirrup.artificialanalysis.ai"><img src="https://img.shields.io/badge/MkDocs-4F46E5?logo=materialformkdocs&logoColor=fff" alt="MkDocs" /></a>
|
|
82
83
|
</p>
|
|
83
84
|
|
|
84
85
|
|
|
@@ -95,6 +96,7 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
95
96
|
- Code execution (local, Docker container, E2B sandbox)
|
|
96
97
|
- MCP client
|
|
97
98
|
- Document input and output
|
|
99
|
+
- **Skills system:** Extend agent capabilities with modular, domain-specific instruction packages
|
|
98
100
|
- **Flexible tool execution:** A generic `Tool` class allows easy tool definition and extension
|
|
99
101
|
- **Context management:** Automatically summarizes conversation history when approaching context limits
|
|
100
102
|
- **Flexible provider support:** Pre-built support for OpenAI-compatible APIs and LiteLLM, or bring your own client
|
|
@@ -104,16 +106,16 @@ Stirrup is a lightweight framework, or starting point template, for building age
|
|
|
104
106
|
|
|
105
107
|
```bash
|
|
106
108
|
# Core framework
|
|
107
|
-
|
|
109
|
+
pip install stirrup # or: uv add stirrup
|
|
108
110
|
|
|
109
111
|
# With all optional components
|
|
110
|
-
|
|
112
|
+
pip install 'stirrup[all]' # or: uv add 'stirrup[all]'
|
|
111
113
|
|
|
112
114
|
# Individual extras
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
116
|
+
pip install 'stirrup[docker]' # or: uv add 'stirrup[docker]'
|
|
117
|
+
pip install 'stirrup[e2b]' # or: uv add 'stirrup[e2b]'
|
|
118
|
+
pip install 'stirrup[mcp]' # or: uv add 'stirrup[mcp]'
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
## Quick Start
|
|
@@ -159,6 +161,24 @@ if __name__ == "__main__":
|
|
|
159
161
|
|
|
160
162
|
> **Note:** This example uses OpenRouter. Set `OPENROUTER_API_KEY` in your environment before running. Web search requires a `BRAVE_API_KEY`. The agent will still work without it, but web search will be unavailable.
|
|
161
163
|
|
|
164
|
+
## Full Customization
|
|
165
|
+
|
|
166
|
+
For using Stirrup as a foundation for your own fully customized agent, you can clone and import Stirrup locally:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Clone the repository
|
|
170
|
+
git clone https://github.com/ArtificialAnalysis/Stirrup.git
|
|
171
|
+
cd stirrup
|
|
172
|
+
|
|
173
|
+
# Install in editable mode
|
|
174
|
+
pip install -e . # or: uv venv && uv pip install -e .
|
|
175
|
+
|
|
176
|
+
# Or with all optional dependencies
|
|
177
|
+
pip install -e '.[all]' # or: uv venv && uv pip install -e '.[all]'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
See the [Full Customization guide](https://stirrup.artificialanalysis.ai/extending/full-customization/) for more details.
|
|
181
|
+
|
|
162
182
|
## How It Works
|
|
163
183
|
|
|
164
184
|
- **`Agent`** - Configures and runs the agent loop until a finish tool is called or max turns reached
|
|
@@ -187,6 +207,7 @@ agent = Agent(client=client, name="deepseek_agent")
|
|
|
187
207
|
### LiteLLM (Anthropic, Google, etc.)
|
|
188
208
|
|
|
189
209
|
```python
|
|
210
|
+
# Ensure LiteLLM is added with: pip install 'stirrup[litellm]' # or: uv add 'stirrup[litellm]'
|
|
190
211
|
# Create LiteLLM client for Anthropic Claude
|
|
191
212
|
# See https://docs.litellm.ai/docs/providers for all supported providers
|
|
192
213
|
client = LiteLLMClient(
|
|
@@ -201,7 +222,7 @@ agent = Agent(
|
|
|
201
222
|
)
|
|
202
223
|
```
|
|
203
224
|
|
|
204
|
-
See [LiteLLM Example](https://artificialanalysis.
|
|
225
|
+
See [LiteLLM Example](https://stirrup.artificialanalysis.ai/examples/#litellm-multi-provider-support) or [Deepseek Example](https://stirrup.artificialanalysis.ai/examples/#openai-compatible-apis-deepseek-vllm-ollama) for complete examples.
|
|
205
226
|
|
|
206
227
|
## Default Tools
|
|
207
228
|
|
|
@@ -284,14 +305,14 @@ agent = Agent(
|
|
|
284
305
|
|
|
285
306
|
## Next Steps
|
|
286
307
|
|
|
287
|
-
- [Getting Started](https://artificialanalysis.
|
|
288
|
-
- [Core Concepts](https://artificialanalysis.
|
|
289
|
-
- [Examples](https://artificialanalysis.
|
|
290
|
-
- [Creating Tools](https://artificialanalysis.
|
|
308
|
+
- [Getting Started](https://stirrup.artificialanalysis.ai/getting-started/) - Installation and first agent tutorial
|
|
309
|
+
- [Core Concepts](https://stirrup.artificialanalysis.ai/concepts/) - Understand Agent, Tools, and Sessions
|
|
310
|
+
- [Examples](https://stirrup.artificialanalysis.ai/examples/) - Working examples for common patterns
|
|
311
|
+
- [Creating Tools](https://stirrup.artificialanalysis.ai/guides/tools/) - Build your own tools
|
|
291
312
|
|
|
292
313
|
## Documentation
|
|
293
314
|
|
|
294
|
-
Full documentation: [artificialanalysis.github.io/Stirrup](https://artificialanalysis.
|
|
315
|
+
Full documentation: [artificialanalysis.github.io/Stirrup](https://stirrup.artificialanalysis.ai)
|
|
295
316
|
|
|
296
317
|
Build and serve locally:
|
|
297
318
|
|
|
@@ -5,21 +5,23 @@ stirrup/clients/litellm_client.py,sha256=J-HDv7ZZTkNYC-aeSNyd7xTDd_5r8DEeXOPz9eQ
|
|
|
5
5
|
stirrup/clients/utils.py,sha256=Yyeh6unQSvqgDTDhjpD5DoRu_wP_nWfsNv9DGXQwgo8,5452
|
|
6
6
|
stirrup/constants.py,sha256=h3NzsePJ4FKpImTpV5xtFeJarKb67jR_6n89tNOkQYs,523
|
|
7
7
|
stirrup/core/__init__.py,sha256=ReBVl7B9h_FNkZ77vCx2xlfuK1JuQ0yTSXrEgc4tONU,39
|
|
8
|
-
stirrup/core/agent.py,sha256=
|
|
8
|
+
stirrup/core/agent.py,sha256=tt1V564B6n_C0ffyH24LuC6PhE4EJA8NOolQCxDG9iw,50836
|
|
9
9
|
stirrup/core/exceptions.py,sha256=CzLVAi7Ns-t9BWSkqQUCB7ypVHAesV2s4a09-i0NXyQ,213
|
|
10
|
-
stirrup/core/models.py,sha256=
|
|
10
|
+
stirrup/core/models.py,sha256=KAyjJIoqbhgy_MN0sPwGI8XWxSiziPnyndMD05ylj_U,21121
|
|
11
11
|
stirrup/prompts/__init__.py,sha256=e4bpTktBaFPuO_bIW5DelGNWtT6_NIUqnD2lRv8n89I,796
|
|
12
12
|
stirrup/prompts/base_system_prompt.txt,sha256=KZ2_JhJ91u4oMqRZvhuAp99nb6ZkXkJdVbIRN6drVME,348
|
|
13
13
|
stirrup/prompts/message_summarizer.txt,sha256=uQoTxreMuC42rTGSZmoH1Dnj06WrEQb0gLkDvVMhosQ,1173
|
|
14
14
|
stirrup/prompts/message_summarizer_bridge.txt,sha256=sWbfnHtI6RWemBIyQsnqHMGpnU-E6FTbfUC6rvkEHLY,372
|
|
15
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
|
|
16
18
|
stirrup/tools/__init__.py,sha256=ohyeMvXb6oURiAyoHi0VmC9ksZSRyGleT341VNzHCy4,2714
|
|
17
19
|
stirrup/tools/calculator.py,sha256=JkuGmGZJtaKbC4vHVrIph4aTjlGcFMhhv5MB1ntqgv4,1278
|
|
18
20
|
stirrup/tools/code_backends/__init__.py,sha256=O3Rs76r0YcQ27voTrx_zuhIEFawK3b1TQdKi70MORG8,987
|
|
19
21
|
stirrup/tools/code_backends/base.py,sha256=Nx0tTDX4GKoBWQK2F953vSsFgWCcOd_1WNtYCA4FG4o,17021
|
|
20
|
-
stirrup/tools/code_backends/docker.py,sha256=
|
|
21
|
-
stirrup/tools/code_backends/e2b.py,sha256=
|
|
22
|
-
stirrup/tools/code_backends/local.py,sha256=
|
|
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
|
|
23
25
|
stirrup/tools/finish.py,sha256=K_NxwOwdvncT2QTua2A_8lZ9MwK4WQQ5FL2gdUrE29c,936
|
|
24
26
|
stirrup/tools/mcp.py,sha256=4wWYae95y8Bs7e36hHwnxRfVVj0PABrsRStw492lLaw,18749
|
|
25
27
|
stirrup/tools/view_image.py,sha256=zazCpZMtLOD6lplLPYGNQ8JeYfc0oUDJoUUyVAp3AMU,3126
|
|
@@ -27,6 +29,6 @@ stirrup/tools/web.py,sha256=2yfBJsu8GgFI7Oh1dFlXwNaXth6WQfGpbJFU-rV-yuI,12261
|
|
|
27
29
|
stirrup/utils/__init__.py,sha256=4kcuExrphSXqgxRgu1q8_Z6Rrb9aAZpIo4Xq4S9Twuk,230
|
|
28
30
|
stirrup/utils/logging.py,sha256=3Li6MhjLJWwaXHDZ06EjgW42XDL6V3ZDt9rccV_ZYZ4,34292
|
|
29
31
|
stirrup/utils/text.py,sha256=3lGlcXFzQ-Mclsbu7wJciG3CcHvQ_Sk98tqOZxYLlGw,479
|
|
30
|
-
stirrup-0.1.
|
|
31
|
-
stirrup-0.1.
|
|
32
|
-
stirrup-0.1.
|
|
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,,
|