agent-weave-lib 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.
@@ -0,0 +1,63 @@
1
+ """agent-weave: Lightweight AI agent framework."""
2
+
3
+ from .agent import Agent
4
+ from .config import AgentSettings
5
+ from .errors import (
6
+ AgentWeaveError,
7
+ ConfigurationError,
8
+ GuardrailViolation,
9
+ LLMBackendError,
10
+ MaxIterationsError,
11
+ TokenBudgetExceededError,
12
+ ToolExecutionError,
13
+ ToolNotFoundError,
14
+ )
15
+ from .guardrails import (
16
+ BlockedWordsGuardrail,
17
+ Guardrail,
18
+ GuardrailPipeline,
19
+ MaxLengthGuardrail,
20
+ PIIGuardrail,
21
+ RegexGuardrail,
22
+ TokenBudgetGuardrail,
23
+ )
24
+ from .models import AgentResult, AgentStep, Message, ToolCall, ToolResult
25
+ from .team import Strategy, Team, TeamResult
26
+ from .tool import Tool, tool
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ # Core
32
+ "Agent",
33
+ "Tool",
34
+ "tool",
35
+ "Team",
36
+ "Strategy",
37
+ # Settings
38
+ "AgentSettings",
39
+ # Models
40
+ "AgentResult",
41
+ "AgentStep",
42
+ "Message",
43
+ "TeamResult",
44
+ "ToolCall",
45
+ "ToolResult",
46
+ # Guardrails
47
+ "Guardrail",
48
+ "GuardrailPipeline",
49
+ "BlockedWordsGuardrail",
50
+ "MaxLengthGuardrail",
51
+ "PIIGuardrail",
52
+ "RegexGuardrail",
53
+ "TokenBudgetGuardrail",
54
+ # Errors
55
+ "AgentWeaveError",
56
+ "ConfigurationError",
57
+ "GuardrailViolation",
58
+ "LLMBackendError",
59
+ "MaxIterationsError",
60
+ "TokenBudgetExceededError",
61
+ "ToolExecutionError",
62
+ "ToolNotFoundError",
63
+ ]
agent_weave/agent.py ADDED
@@ -0,0 +1,177 @@
1
+ """Core Agent class for agent-weave."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Sequence
6
+
7
+ from .config import AgentSettings
8
+ from .errors import ConfigurationError
9
+ from .guardrails import (
10
+ Guardrail,
11
+ GuardrailPipeline,
12
+ TokenBudgetGuardrail,
13
+ )
14
+ from .llm.base import LLMBackend
15
+ from .memory import ConversationMemory, Memory
16
+ from .models import AgentResult, Message
17
+ from .react import ReActEngine
18
+ from .tool import Tool
19
+
20
+
21
+ class Agent:
22
+ """A self-contained AI agent with tools, memory, and guardrails.
23
+
24
+ Usage::
25
+
26
+ from agent_weave import Agent, tool
27
+ from agent_weave.llm.openai_backend import OpenAIBackend
28
+
29
+ @tool(description="Search the web")
30
+ def web_search(query: str) -> str:
31
+ return f"Results for: {query}"
32
+
33
+ agent = Agent(
34
+ name="researcher",
35
+ llm=OpenAIBackend(api_key="sk-..."),
36
+ tools=[web_search],
37
+ system_prompt="You are a research assistant.",
38
+ )
39
+
40
+ result = agent.run("Find the latest AI trends in 2025")
41
+ print(result.output)
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ name: str,
47
+ *,
48
+ llm: LLMBackend,
49
+ tools: Sequence[Tool] | None = None,
50
+ system_prompt: str = "You are a helpful AI assistant.",
51
+ memory: Memory | None = None,
52
+ model: str | None = None,
53
+ temperature: float | None = None,
54
+ max_iterations: int | None = None,
55
+ token_budget: int | None = None,
56
+ output_guardrails: list[Guardrail] | None = None,
57
+ settings: AgentSettings | None = None,
58
+ verbose: bool | None = None,
59
+ ) -> None:
60
+ if not name.strip():
61
+ raise ConfigurationError("Agent name cannot be empty.")
62
+
63
+ self.name = name.strip()
64
+ self._llm = llm
65
+ self._tools = list(tools or [])
66
+ self._system_prompt = system_prompt
67
+ self._memory = memory or ConversationMemory()
68
+ self._settings = settings or AgentSettings()
69
+
70
+ self._model = model or self._settings.default_model
71
+ self._temperature = temperature
72
+ self._max_iterations = max_iterations or self._settings.max_iterations
73
+ self._verbose = verbose if verbose is not None else self._settings.verbose
74
+
75
+ # Guardrails.
76
+ self._token_budget: TokenBudgetGuardrail | None = None
77
+ if token_budget or self._settings.token_budget:
78
+ self._token_budget = TokenBudgetGuardrail(
79
+ budget=token_budget or self._settings.token_budget or 0
80
+ )
81
+
82
+ self._output_guardrails: GuardrailPipeline | None = None
83
+ if output_guardrails:
84
+ self._output_guardrails = GuardrailPipeline(output_guardrails)
85
+
86
+ @property
87
+ def tools(self) -> list[Tool]:
88
+ return list(self._tools)
89
+
90
+ @property
91
+ def tool_names(self) -> list[str]:
92
+ return [t.name for t in self._tools]
93
+
94
+ def add_tool(self, tool_instance: Tool) -> None:
95
+ """Register an additional tool."""
96
+ self._tools.append(tool_instance)
97
+
98
+ def run(self, task: str) -> AgentResult:
99
+ """Run the agent on a task (synchronous).
100
+
101
+ Sets up memory with system prompt + user task, then
102
+ runs the ReAct loop until completion.
103
+ """
104
+ self._prepare_memory(task)
105
+
106
+ engine = self._build_engine()
107
+ return engine.run(self.name)
108
+
109
+ async def arun(self, task: str) -> AgentResult:
110
+ """Run the agent on a task (asynchronous)."""
111
+ self._prepare_memory(task)
112
+
113
+ engine = self._build_engine()
114
+ return await engine.arun(self.name)
115
+
116
+ def chat(self, message: str) -> AgentResult:
117
+ """Continue a conversation (preserves history).
118
+
119
+ Unlike ``run()``, this does NOT reset memory. It appends
120
+ the new user message and continues the conversation.
121
+ """
122
+ # Ensure system prompt exists on first call.
123
+ if self._memory.size == 0:
124
+ self._memory.add(
125
+ Message(role="system", content=self._system_prompt)
126
+ )
127
+
128
+ self._memory.add(Message(role="user", content=message))
129
+
130
+ engine = self._build_engine()
131
+ return engine.run(self.name)
132
+
133
+ async def achat(self, message: str) -> AgentResult:
134
+ """Continue a conversation (async, preserves history)."""
135
+ if self._memory.size == 0:
136
+ self._memory.add(
137
+ Message(role="system", content=self._system_prompt)
138
+ )
139
+
140
+ self._memory.add(Message(role="user", content=message))
141
+
142
+ engine = self._build_engine()
143
+ return await engine.arun(self.name)
144
+
145
+ def reset(self) -> None:
146
+ """Clear memory and token budget."""
147
+ self._memory.clear()
148
+ if self._token_budget:
149
+ self._token_budget.reset()
150
+
151
+ def _prepare_memory(self, task: str) -> None:
152
+ """Reset memory and load system prompt + user task."""
153
+ self._memory.clear()
154
+ if self._token_budget:
155
+ self._token_budget.reset()
156
+
157
+ self._memory.add(Message(role="system", content=self._system_prompt))
158
+ self._memory.add(Message(role="user", content=task))
159
+
160
+ def _build_engine(self) -> ReActEngine:
161
+ return ReActEngine(
162
+ llm=self._llm,
163
+ tools=self._tools,
164
+ memory=self._memory,
165
+ max_iterations=self._max_iterations,
166
+ model=self._model,
167
+ temperature=self._temperature,
168
+ token_budget=self._token_budget,
169
+ output_guardrails=self._output_guardrails,
170
+ verbose=self._verbose,
171
+ )
172
+
173
+ def __repr__(self) -> str:
174
+ return (
175
+ f"Agent(name={self.name!r}, model={self._model!r}, "
176
+ f"tools={self.tool_names})"
177
+ )
agent_weave/cli.py ADDED
@@ -0,0 +1,109 @@
1
+ """CLI interface for agent-weave."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+
10
+
11
+ def _get_agent():
12
+ """Build a default agent from environment variables."""
13
+ from .agent import Agent
14
+ from .llm.openai_backend import OpenAIBackend
15
+
16
+ api_key = os.getenv("OPENAI_API_KEY")
17
+ if not api_key:
18
+ print("Error: OPENAI_API_KEY environment variable is required.", file=sys.stderr)
19
+ sys.exit(1)
20
+
21
+ model = os.getenv("AGENTWEAVE_DEFAULT_MODEL", "gpt-4o-mini")
22
+
23
+ backend = OpenAIBackend(api_key=api_key, default_model=model)
24
+ return Agent(
25
+ name="cli-agent",
26
+ llm=backend,
27
+ system_prompt="You are a helpful AI assistant. Be concise.",
28
+ model=model,
29
+ )
30
+
31
+
32
+ def cmd_run(args: argparse.Namespace) -> None:
33
+ """Run a task with the agent."""
34
+ agent = _get_agent()
35
+ result = agent.run(args.task)
36
+ print(result.output)
37
+
38
+ if args.verbose:
39
+ print(f"\n--- Stats ---")
40
+ print(f"Iterations: {result.total_iterations}")
41
+ print(f"Tokens: {result.total_tokens:,}")
42
+ print(f"Tool calls: {result.total_tool_calls}")
43
+
44
+
45
+ def cmd_chat(args: argparse.Namespace) -> None:
46
+ """Interactive chat with the agent."""
47
+ agent = _get_agent()
48
+ print("Agent Weave Chat (type 'quit' to exit)")
49
+ print("-" * 40)
50
+
51
+ while True:
52
+ try:
53
+ user_input = input("\nYou: ").strip()
54
+ except (EOFError, KeyboardInterrupt):
55
+ print("\nGoodbye!")
56
+ break
57
+
58
+ if user_input.lower() in ("quit", "exit", "q"):
59
+ print("Goodbye!")
60
+ break
61
+
62
+ if not user_input:
63
+ continue
64
+
65
+ result = agent.chat(user_input)
66
+ print(f"\nAgent: {result.output}")
67
+
68
+
69
+ def cmd_info(args: argparse.Namespace) -> None:
70
+ """Show library info."""
71
+ from . import __version__
72
+ info = {
73
+ "name": "agent-weave",
74
+ "version": __version__,
75
+ "python": sys.version,
76
+ }
77
+ print(json.dumps(info, indent=2))
78
+
79
+
80
+ def main() -> None:
81
+ parser = argparse.ArgumentParser(
82
+ prog="agent-weave",
83
+ description="Lightweight AI agent framework.",
84
+ )
85
+ subparsers = parser.add_subparsers(dest="command")
86
+
87
+ # Run command.
88
+ run_parser = subparsers.add_parser("run", help="Run a task")
89
+ run_parser.add_argument("task", help="The task to execute")
90
+ run_parser.add_argument("-v", "--verbose", action="store_true")
91
+ run_parser.set_defaults(func=cmd_run)
92
+
93
+ # Chat command.
94
+ chat_parser = subparsers.add_parser("chat", help="Interactive chat")
95
+ chat_parser.set_defaults(func=cmd_chat)
96
+
97
+ # Info command.
98
+ info_parser = subparsers.add_parser("info", help="Show library info")
99
+ info_parser.set_defaults(func=cmd_info)
100
+
101
+ args = parser.parse_args()
102
+ if hasattr(args, "func"):
103
+ args.func(args)
104
+ else:
105
+ parser.print_help()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
agent_weave/config.py ADDED
@@ -0,0 +1,51 @@
1
+ """Configuration for agent-weave."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+
8
+
9
+ def _as_bool(value: str | None, *, default: bool) -> bool:
10
+ if value is None:
11
+ return default
12
+ return value.strip().lower() in {"1", "true", "yes", "on"}
13
+
14
+
15
+ def _as_float(value: str | None, *, default: float | None) -> float | None:
16
+ if value is None or value.strip() == "":
17
+ return default
18
+ return float(value)
19
+
20
+
21
+ def _as_int(value: str | None, *, default: int) -> int:
22
+ if value is None or value.strip() == "":
23
+ return default
24
+ return int(value)
25
+
26
+
27
+ @dataclass
28
+ class AgentSettings:
29
+ """Global settings for agent-weave."""
30
+
31
+ default_model: str = "gpt-4o-mini"
32
+ max_iterations: int = 10
33
+ request_timeout: float | None = 60.0
34
+ token_budget: int | None = None
35
+ verbose: bool = False
36
+
37
+ @classmethod
38
+ def from_env(cls) -> "AgentSettings":
39
+ return cls(
40
+ default_model=os.getenv("AGENTWEAVE_DEFAULT_MODEL", "gpt-4o-mini"),
41
+ max_iterations=_as_int(
42
+ os.getenv("AGENTWEAVE_MAX_ITERATIONS"), default=10
43
+ ),
44
+ request_timeout=_as_float(
45
+ os.getenv("AGENTWEAVE_REQUEST_TIMEOUT"), default=60.0
46
+ ),
47
+ token_budget=_as_int(
48
+ os.getenv("AGENTWEAVE_TOKEN_BUDGET"), default=0
49
+ ) or None,
50
+ verbose=_as_bool(os.getenv("AGENTWEAVE_VERBOSE"), default=False),
51
+ )
agent_weave/errors.py ADDED
@@ -0,0 +1,64 @@
1
+ """Custom exceptions for agent-weave."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class AgentWeaveError(Exception):
7
+ """Base exception for agent-weave."""
8
+
9
+
10
+ class ToolExecutionError(AgentWeaveError):
11
+ """Raised when a tool fails during execution."""
12
+
13
+ def __init__(self, tool_name: str, message: str) -> None:
14
+ self.tool_name = tool_name
15
+ super().__init__(f"Tool '{tool_name}' failed: {message}")
16
+
17
+
18
+ class ToolNotFoundError(AgentWeaveError):
19
+ """Raised when a requested tool is not registered."""
20
+
21
+ def __init__(self, tool_name: str, available: list[str] | None = None) -> None:
22
+ self.tool_name = tool_name
23
+ avail = ", ".join(available) if available else "none"
24
+ super().__init__(
25
+ f"Tool '{tool_name}' not found. Available tools: {avail}"
26
+ )
27
+
28
+
29
+ class MaxIterationsError(AgentWeaveError):
30
+ """Raised when the ReAct loop exceeds the maximum iterations."""
31
+
32
+ def __init__(self, max_iterations: int) -> None:
33
+ self.max_iterations = max_iterations
34
+ super().__init__(
35
+ f"Agent exceeded maximum iterations ({max_iterations}). "
36
+ "Increase max_iterations or simplify the task."
37
+ )
38
+
39
+
40
+ class TokenBudgetExceededError(AgentWeaveError):
41
+ """Raised when the token budget is exhausted."""
42
+
43
+ def __init__(self, budget: int, used: int) -> None:
44
+ self.budget = budget
45
+ self.used = used
46
+ super().__init__(
47
+ f"Token budget exceeded: used {used:,} of {budget:,} allowed tokens."
48
+ )
49
+
50
+
51
+ class LLMBackendError(AgentWeaveError):
52
+ """Raised when the LLM backend fails."""
53
+
54
+
55
+ class GuardrailViolation(AgentWeaveError):
56
+ """Raised when a guardrail check fails."""
57
+
58
+ def __init__(self, guardrail_name: str, message: str) -> None:
59
+ self.guardrail_name = guardrail_name
60
+ super().__init__(f"Guardrail '{guardrail_name}': {message}")
61
+
62
+
63
+ class ConfigurationError(AgentWeaveError):
64
+ """Raised for invalid configuration."""
@@ -0,0 +1,144 @@
1
+ """Guardrails for agent-weave — input/output validators and budget controls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass, field
8
+ from typing import Any
9
+
10
+ from .errors import GuardrailViolation, TokenBudgetExceededError
11
+
12
+
13
+ class Guardrail(ABC):
14
+ """Base class for all guardrails."""
15
+
16
+ name: str = "base"
17
+
18
+ @abstractmethod
19
+ def check(self, value: str, *, context: dict[str, Any] | None = None) -> str:
20
+ """Validate and optionally transform the value.
21
+
22
+ Returns the (possibly modified) value.
23
+ Raises ``GuardrailViolation`` if the check fails.
24
+ """
25
+
26
+
27
+ class MaxLengthGuardrail(Guardrail):
28
+ """Reject output that exceeds a character limit."""
29
+
30
+ name = "max_length"
31
+
32
+ def __init__(self, max_chars: int = 10_000) -> None:
33
+ self._max = max_chars
34
+
35
+ def check(self, value: str, *, context: dict[str, Any] | None = None) -> str:
36
+ if len(value) > self._max:
37
+ raise GuardrailViolation(
38
+ self.name,
39
+ f"Output length {len(value):,} exceeds limit of {self._max:,} chars.",
40
+ )
41
+ return value
42
+
43
+
44
+ class BlockedWordsGuardrail(Guardrail):
45
+ """Reject output that contains any blocked words."""
46
+
47
+ name = "blocked_words"
48
+
49
+ def __init__(self, words: list[str]) -> None:
50
+ self._words = [w.lower() for w in words]
51
+
52
+ def check(self, value: str, *, context: dict[str, Any] | None = None) -> str:
53
+ lower_val = value.lower()
54
+ for word in self._words:
55
+ if word in lower_val:
56
+ raise GuardrailViolation(
57
+ self.name, f"Blocked word detected: '{word}'"
58
+ )
59
+ return value
60
+
61
+
62
+ class RegexGuardrail(Guardrail):
63
+ """Reject output that matches a forbidden regex pattern."""
64
+
65
+ name = "regex_filter"
66
+
67
+ def __init__(self, pattern: str, *, message: str = "Forbidden pattern detected.") -> None:
68
+ self._pattern = re.compile(pattern, re.IGNORECASE)
69
+ self._message = message
70
+
71
+ def check(self, value: str, *, context: dict[str, Any] | None = None) -> str:
72
+ if self._pattern.search(value):
73
+ raise GuardrailViolation(self.name, self._message)
74
+ return value
75
+
76
+
77
+ class PIIGuardrail(Guardrail):
78
+ """Detect common PII patterns (emails, phone numbers, SSNs)."""
79
+
80
+ name = "pii_filter"
81
+
82
+ _PATTERNS = {
83
+ "email": re.compile(r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"),
84
+ "phone": re.compile(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b"),
85
+ "ssn": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
86
+ }
87
+
88
+ def __init__(self, *, redact: bool = False) -> None:
89
+ self._redact = redact
90
+
91
+ def check(self, value: str, *, context: dict[str, Any] | None = None) -> str:
92
+ for pii_type, pattern in self._PATTERNS.items():
93
+ if pattern.search(value):
94
+ if self._redact:
95
+ value = pattern.sub(f"[REDACTED_{pii_type.upper()}]", value)
96
+ else:
97
+ raise GuardrailViolation(
98
+ self.name,
99
+ f"PII detected: {pii_type}. Set redact=True to auto-redact.",
100
+ )
101
+ return value
102
+
103
+
104
+ @dataclass
105
+ class TokenBudgetGuardrail:
106
+ """Track and enforce a token budget across the agent run."""
107
+
108
+ budget: int
109
+ _used: int = field(default=0, init=False)
110
+
111
+ @property
112
+ def used(self) -> int:
113
+ return self._used
114
+
115
+ @property
116
+ def remaining(self) -> int:
117
+ return max(0, self.budget - self._used)
118
+
119
+ def consume(self, tokens: int) -> None:
120
+ """Record token usage. Raises if budget exceeded."""
121
+ self._used += tokens
122
+ if self._used > self.budget:
123
+ raise TokenBudgetExceededError(self.budget, self._used)
124
+
125
+ def reset(self) -> None:
126
+ self._used = 0
127
+
128
+
129
+ class GuardrailPipeline:
130
+ """Run a sequence of guardrails on a value."""
131
+
132
+ def __init__(self, guardrails: list[Guardrail] | None = None) -> None:
133
+ self._guardrails = list(guardrails or [])
134
+
135
+ def add(self, guardrail: Guardrail) -> "GuardrailPipeline":
136
+ self._guardrails.append(guardrail)
137
+ return self
138
+
139
+ def run(self, value: str, *, context: dict[str, Any] | None = None) -> str:
140
+ """Run all guardrails in sequence. Returns the (possibly modified) value."""
141
+ result = value
142
+ for rail in self._guardrails:
143
+ result = rail.check(result, context=context)
144
+ return result
@@ -0,0 +1,5 @@
1
+ """LLM backend modules for agent-weave."""
2
+
3
+ from .base import LLMBackend
4
+
5
+ __all__ = ["LLMBackend"]