maque-core 1.3.6__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.
- maque_core/__init__.py +28 -0
- maque_core/config.py +80 -0
- maque_core/contracts/__init__.py +17 -0
- maque_core/contracts/llm.py +47 -0
- maque_core/contracts/messages.py +264 -0
- maque_core/contracts/tool.py +121 -0
- maque_core/factory.py +105 -0
- maque_core/kernel/__init__.py +8 -0
- maque_core/kernel/agent.py +228 -0
- maque_core/kernel/budget.py +69 -0
- maque_core/kernel/errors.py +46 -0
- maque_core/kernel/loop.py +320 -0
- maque_core/llm/__init__.py +39 -0
- maque_core/llm/errors.py +258 -0
- maque_core/llm/mock.py +91 -0
- maque_core/llm/openai_compatible.py +309 -0
- maque_core/tools/__init__.py +19 -0
- maque_core/tools/_global_registry.py +38 -0
- maque_core/tools/decorator.py +256 -0
- maque_core/tools/discovery.py +105 -0
- maque_core/tools/executor.py +147 -0
- maque_core/tools/impl/__init__.py +1 -0
- maque_core/tools/impl/add_tool.py +17 -0
- maque_core/tools/impl/echo_tool.py +19 -0
- maque_core/tools/registry.py +299 -0
- maque_core-1.3.6.dist-info/METADATA +612 -0
- maque_core-1.3.6.dist-info/RECORD +31 -0
- maque_core-1.3.6.dist-info/WHEEL +5 -0
- maque_core-1.3.6.dist-info/entry_points.txt +2 -0
- maque_core-1.3.6.dist-info/licenses/LICENSE +21 -0
- maque_core-1.3.6.dist-info/top_level.txt +1 -0
maque_core/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""maque_core — a lightweight, extensible Agent framework."""
|
|
2
|
+
|
|
3
|
+
from .contracts.llm import LLMProvider
|
|
4
|
+
from .contracts.messages import LLMResponse, Message, Role, ToolCall
|
|
5
|
+
from .contracts.tool import ToolSpec
|
|
6
|
+
from .factory import create_agent
|
|
7
|
+
from .kernel import Agent, AgentRunError
|
|
8
|
+
from .llm import MockLLM, OpenAICompatibleProvider
|
|
9
|
+
from .tools import ToolRegistry, tool
|
|
10
|
+
|
|
11
|
+
__version__ = "1.3.4"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"create_agent",
|
|
15
|
+
"Agent",
|
|
16
|
+
"AgentRunError",
|
|
17
|
+
"tool",
|
|
18
|
+
"LLMProvider",
|
|
19
|
+
"Message",
|
|
20
|
+
"Role",
|
|
21
|
+
"ToolCall",
|
|
22
|
+
"LLMResponse",
|
|
23
|
+
"ToolSpec",
|
|
24
|
+
"ToolRegistry",
|
|
25
|
+
"OpenAICompatibleProvider",
|
|
26
|
+
"MockLLM",
|
|
27
|
+
"__version__",
|
|
28
|
+
]
|
maque_core/config.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Configuration for the agent framework.
|
|
2
|
+
|
|
3
|
+
All settings can be overridden via environment variables with the
|
|
4
|
+
``MAQUE_`` prefix, e.g. ``MAQUE_LLM_MODEL=qwen3-turbo``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class LLMConfig:
|
|
16
|
+
"""LLM backend configuration."""
|
|
17
|
+
|
|
18
|
+
model: str = "qwen-turbo"
|
|
19
|
+
base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
20
|
+
api_key: str = ""
|
|
21
|
+
temperature: float = 0.7
|
|
22
|
+
timeout: int = 60
|
|
23
|
+
max_tokens: int = 2048
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_env(cls) -> "LLMConfig":
|
|
27
|
+
return cls(
|
|
28
|
+
model=os.environ.get("MAQUE_LLM_MODEL", cls.model),
|
|
29
|
+
base_url=os.environ.get("MAQUE_LLM_BASE_URL", cls.base_url),
|
|
30
|
+
api_key=os.environ.get("MAQUE_LLM_API_KEY", cls.api_key),
|
|
31
|
+
temperature=float(os.environ.get("MAQUE_LLM_TEMPERATURE", cls.temperature)),
|
|
32
|
+
timeout=int(os.environ.get("MAQUE_LLM_TIMEOUT", cls.timeout)),
|
|
33
|
+
max_tokens=int(os.environ.get("MAQUE_LLM_MAX_TOKENS", cls.max_tokens)),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class AgentConfig:
|
|
39
|
+
"""Agent runtime configuration."""
|
|
40
|
+
|
|
41
|
+
system_prompt: str = "You are a helpful assistant with access to tools. Use tools when needed to answer questions accurately."
|
|
42
|
+
max_iterations: int = 10
|
|
43
|
+
max_tool_calls: Optional[int] = None
|
|
44
|
+
verbose: bool = False
|
|
45
|
+
max_history_turns: int = 20
|
|
46
|
+
include_reasoning_in_history: bool = True
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_env(cls) -> "AgentConfig":
|
|
50
|
+
max_tool_calls_str = os.environ.get("MAQUE_MAX_TOOL_CALLS", "")
|
|
51
|
+
max_tool_calls = int(max_tool_calls_str) if max_tool_calls_str else None
|
|
52
|
+
max_history_str = os.environ.get("MAQUE_MAX_HISTORY_TURNS", "")
|
|
53
|
+
max_history_turns = int(max_history_str) if max_history_str else cls.max_history_turns
|
|
54
|
+
return cls(
|
|
55
|
+
system_prompt=os.environ.get("MAQUE_SYSTEM_PROMPT", cls.system_prompt),
|
|
56
|
+
max_iterations=int(os.environ.get("MAQUE_MAX_ITERATIONS", cls.max_iterations)),
|
|
57
|
+
max_tool_calls=max_tool_calls,
|
|
58
|
+
verbose=os.environ.get("MAQUE_VERBOSE", "").lower() in ("1", "true", "yes"),
|
|
59
|
+
max_history_turns=max_history_turns,
|
|
60
|
+
include_reasoning_in_history=os.environ.get(
|
|
61
|
+
"MAQUE_INCLUDE_REASONING_IN_HISTORY", "true"
|
|
62
|
+
).lower() in ("1", "true", "yes"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AppConfig:
|
|
68
|
+
"""Top-level application configuration."""
|
|
69
|
+
|
|
70
|
+
llm: LLMConfig = field(default_factory=LLMConfig)
|
|
71
|
+
agent: AgentConfig = field(default_factory=AgentConfig)
|
|
72
|
+
tools_package: str = "maque_core.tools.impl"
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_env(cls) -> "AppConfig":
|
|
76
|
+
return cls(
|
|
77
|
+
llm=LLMConfig.from_env(),
|
|
78
|
+
agent=AgentConfig.from_env(),
|
|
79
|
+
tools_package=os.environ.get("MAQUE_TOOLS_PACKAGE", cls.tools_package),
|
|
80
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Core contracts (interfaces and data structures) for the agent framework."""
|
|
2
|
+
|
|
3
|
+
from .messages import Role, ToolCall, Message, LLMResponse, Observation
|
|
4
|
+
from .llm import LLMProvider
|
|
5
|
+
from .tool import ToolSpec, ToolRegistry, CallBudget
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Role",
|
|
9
|
+
"ToolCall",
|
|
10
|
+
"Message",
|
|
11
|
+
"LLMResponse",
|
|
12
|
+
"Observation",
|
|
13
|
+
"LLMProvider",
|
|
14
|
+
"ToolSpec",
|
|
15
|
+
"ToolRegistry",
|
|
16
|
+
"CallBudget",
|
|
17
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""LLM provider protocol — the interface every LLM backend must implement.
|
|
2
|
+
|
|
3
|
+
Using a Protocol (structural subtyping) means providers don't need to
|
|
4
|
+
inherit from a base class — they just need the right method signature.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING, Any, List, Optional, Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .messages import LLMResponse, Message, ToolSpec
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class LLMProvider(Protocol):
|
|
17
|
+
"""Protocol for LLM backends.
|
|
18
|
+
|
|
19
|
+
Any class with a compatible ``chat`` method is automatically an
|
|
20
|
+
LLMProvider — no explicit inheritance required.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def chat(
|
|
24
|
+
self,
|
|
25
|
+
messages: List["Message"],
|
|
26
|
+
tools: Optional[List["ToolSpec"]] = None,
|
|
27
|
+
max_tokens: Optional[int] = None,
|
|
28
|
+
raise_on_error: bool = False,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
) -> "LLMResponse":
|
|
31
|
+
"""Send a chat completion request and return the response.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
messages: The conversation history.
|
|
35
|
+
tools: Optional list of tool specs the model may call.
|
|
36
|
+
max_tokens: Maximum number of tokens to generate. ``None``
|
|
37
|
+
means "use the provider's configured default".
|
|
38
|
+
raise_on_error: When ``False`` (default), API failures are
|
|
39
|
+
returned as an :class:`LLMResponse` with ``error`` set.
|
|
40
|
+
When ``True``, they are raised as an
|
|
41
|
+
:class:`~maque_core.llm.errors.LLMError` subclass instead.
|
|
42
|
+
**kwargs: Backend-specific parameters (temperature, model, etc.).
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
An LLMResponse with content and/or tool_calls.
|
|
46
|
+
"""
|
|
47
|
+
...
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Core message data structures for agent communication.
|
|
2
|
+
|
|
3
|
+
Defines the universal message types used across all layers:
|
|
4
|
+
- Role: who sent a message
|
|
5
|
+
- ToolCall: a requested tool invocation
|
|
6
|
+
- Message: a chat message (system / user / assistant / tool)
|
|
7
|
+
- LLMResponse: what the LLM returns
|
|
8
|
+
- Observation: the result of a tool call
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Role(str, Enum):
|
|
20
|
+
"""Message role — who sent the message."""
|
|
21
|
+
|
|
22
|
+
SYSTEM = "system"
|
|
23
|
+
USER = "user"
|
|
24
|
+
ASSISTANT = "assistant"
|
|
25
|
+
TOOL = "tool"
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return self.value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ToolCall:
|
|
33
|
+
"""A single tool invocation requested by the model.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
id: Unique identifier for this tool call (used to match results).
|
|
37
|
+
name: Name of the tool to invoke.
|
|
38
|
+
arguments: Dict of argument name → value to pass to the tool.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
id: str
|
|
42
|
+
name: str
|
|
43
|
+
arguments: Dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
return {
|
|
47
|
+
"id": self.id,
|
|
48
|
+
"name": self.name,
|
|
49
|
+
"arguments": self.arguments,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ToolCall":
|
|
54
|
+
return cls(
|
|
55
|
+
id=data["id"],
|
|
56
|
+
name=data["name"],
|
|
57
|
+
arguments=data.get("arguments", {}),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Message:
|
|
63
|
+
"""A single chat message.
|
|
64
|
+
|
|
65
|
+
Use factory methods for readability:
|
|
66
|
+
Message.system("You are helpful.")
|
|
67
|
+
Message.user("Hello")
|
|
68
|
+
Message.assistant("Hi there!", tool_calls=[...])
|
|
69
|
+
Message.tool_result("call_1", "echo", "output text")
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
reasoning: Optional reasoning / chain-of-thought content
|
|
73
|
+
associated with this message. Used for multi-turn
|
|
74
|
+
conversations so that subsequent rounds can see the
|
|
75
|
+
model's previous reasoning basis. None when the message
|
|
76
|
+
has no reasoning (system / user / tool messages, or
|
|
77
|
+
when ``include_reasoning_in_history`` is False).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
role: Role
|
|
81
|
+
content: str = ""
|
|
82
|
+
tool_calls: List[ToolCall] = field(default_factory=list)
|
|
83
|
+
tool_call_id: Optional[str] = None
|
|
84
|
+
tool_name: Optional[str] = None
|
|
85
|
+
reasoning: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
# ---- factory methods ----
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def system(cls, content: str) -> "Message":
|
|
91
|
+
return cls(role=Role.SYSTEM, content=content)
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def user(cls, content: str) -> "Message":
|
|
95
|
+
return cls(role=Role.USER, content=content)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def assistant(
|
|
99
|
+
cls,
|
|
100
|
+
content: str = "",
|
|
101
|
+
tool_calls: Optional[List[ToolCall]] = None,
|
|
102
|
+
) -> "Message":
|
|
103
|
+
return cls(
|
|
104
|
+
role=Role.ASSISTANT,
|
|
105
|
+
content=content,
|
|
106
|
+
tool_calls=tool_calls or [],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def tool_result(
|
|
111
|
+
cls,
|
|
112
|
+
tool_call_id: str,
|
|
113
|
+
tool_name: str,
|
|
114
|
+
content: str,
|
|
115
|
+
) -> "Message":
|
|
116
|
+
return cls(
|
|
117
|
+
role=Role.TOOL,
|
|
118
|
+
content=content,
|
|
119
|
+
tool_call_id=tool_call_id,
|
|
120
|
+
tool_name=tool_name,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# ---- serialization ----
|
|
124
|
+
|
|
125
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
126
|
+
data: Dict[str, Any] = {"role": str(self.role), "content": self.content}
|
|
127
|
+
if self.tool_calls:
|
|
128
|
+
data["tool_calls"] = [tc.to_dict() for tc in self.tool_calls]
|
|
129
|
+
if self.tool_call_id is not None:
|
|
130
|
+
data["tool_call_id"] = self.tool_call_id
|
|
131
|
+
if self.tool_name is not None:
|
|
132
|
+
data["name"] = self.tool_name
|
|
133
|
+
if self.reasoning is not None:
|
|
134
|
+
data["reasoning"] = self.reasoning
|
|
135
|
+
return data
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Message":
|
|
139
|
+
role = Role(data["role"])
|
|
140
|
+
tool_calls = [
|
|
141
|
+
ToolCall.from_dict(tc) for tc in data.get("tool_calls", [])
|
|
142
|
+
]
|
|
143
|
+
return cls(
|
|
144
|
+
role=role,
|
|
145
|
+
content=data.get("content", ""),
|
|
146
|
+
tool_calls=tool_calls,
|
|
147
|
+
tool_call_id=data.get("tool_call_id"),
|
|
148
|
+
tool_name=data.get("name"),
|
|
149
|
+
reasoning=data.get("reasoning"),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class LLMResponse:
|
|
155
|
+
"""The result of an LLM chat completion call.
|
|
156
|
+
|
|
157
|
+
Attributes:
|
|
158
|
+
content: The text response (may be empty if only tool calls).
|
|
159
|
+
When the call failed and the adapter returned instead of
|
|
160
|
+
raising, ``content`` holds the human-readable error text.
|
|
161
|
+
reasoning: The model's internal reasoning / chain-of-thought.
|
|
162
|
+
None if the model did not produce reasoning content.
|
|
163
|
+
tool_calls: Tool invocations requested by the model.
|
|
164
|
+
model: Name of the model that generated the response.
|
|
165
|
+
usage: Optional token usage info.
|
|
166
|
+
finish_reason: Why the model stopped generating. Passed through
|
|
167
|
+
from the API verbatim (``stop`` / ``length`` / ``tool_calls`` /
|
|
168
|
+
``content_filter`` ...). ``"length"`` means the answer was
|
|
169
|
+
truncated by the token budget — it is NOT a complete answer.
|
|
170
|
+
None when the provider did not report one (e.g. on failure).
|
|
171
|
+
error: Error description when the call failed. ``None`` means the
|
|
172
|
+
response is a genuine model output. When set, ``content`` also
|
|
173
|
+
carries the message so it can still be displayed, but callers
|
|
174
|
+
MUST NOT treat it as an answer — check this field, or
|
|
175
|
+
``response.is_error``.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
content: str = ""
|
|
179
|
+
reasoning: Optional[str] = None
|
|
180
|
+
tool_calls: List[ToolCall] = field(default_factory=list)
|
|
181
|
+
model: str = ""
|
|
182
|
+
usage: Optional[Dict[str, int]] = None
|
|
183
|
+
finish_reason: Optional[str] = None
|
|
184
|
+
error: Optional[str] = None
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def is_error(self) -> bool:
|
|
188
|
+
"""True when this response represents a failed LLM call."""
|
|
189
|
+
return self.error is not None
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def is_truncated(self) -> bool:
|
|
193
|
+
"""True when the model stopped because it hit the token limit."""
|
|
194
|
+
return self.finish_reason == "length"
|
|
195
|
+
|
|
196
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
197
|
+
data = {
|
|
198
|
+
"content": self.content,
|
|
199
|
+
"reasoning": self.reasoning,
|
|
200
|
+
"tool_calls": [tc.to_dict() for tc in self.tool_calls],
|
|
201
|
+
"model": self.model,
|
|
202
|
+
"usage": self.usage,
|
|
203
|
+
"finish_reason": self.finish_reason,
|
|
204
|
+
"error": self.error,
|
|
205
|
+
}
|
|
206
|
+
return data
|
|
207
|
+
|
|
208
|
+
@classmethod
|
|
209
|
+
def from_dict(cls, data: Dict[str, Any]) -> "LLMResponse":
|
|
210
|
+
return cls(
|
|
211
|
+
content=data.get("content", ""),
|
|
212
|
+
reasoning=data.get("reasoning"),
|
|
213
|
+
tool_calls=[ToolCall.from_dict(tc) for tc in data.get("tool_calls", [])],
|
|
214
|
+
model=data.get("model", ""),
|
|
215
|
+
usage=data.get("usage"),
|
|
216
|
+
finish_reason=data.get("finish_reason"),
|
|
217
|
+
error=data.get("error"),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class Observation:
|
|
223
|
+
"""The result of executing a tool call.
|
|
224
|
+
|
|
225
|
+
Attributes:
|
|
226
|
+
tool_call_id: Matches the id of the originating ToolCall.
|
|
227
|
+
tool_name: Name of the tool that was executed.
|
|
228
|
+
content: String output of the tool.
|
|
229
|
+
is_error: Whether the tool call resulted in an error.
|
|
230
|
+
metadata: Extra info like execution duration, etc.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
tool_call_id: str
|
|
234
|
+
tool_name: str
|
|
235
|
+
content: str
|
|
236
|
+
is_error: bool = False
|
|
237
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
238
|
+
|
|
239
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
240
|
+
return {
|
|
241
|
+
"tool_call_id": self.tool_call_id,
|
|
242
|
+
"tool_name": self.tool_name,
|
|
243
|
+
"content": self.content,
|
|
244
|
+
"is_error": self.is_error,
|
|
245
|
+
"metadata": self.metadata,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Observation":
|
|
250
|
+
return cls(
|
|
251
|
+
tool_call_id=data["tool_call_id"],
|
|
252
|
+
tool_name=data["tool_name"],
|
|
253
|
+
content=data["content"],
|
|
254
|
+
is_error=data.get("is_error", False),
|
|
255
|
+
metadata=data.get("metadata", {}),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def to_message(self) -> Message:
|
|
259
|
+
"""Convert to a tool-role Message for feeding back to the LLM."""
|
|
260
|
+
return Message.tool_result(
|
|
261
|
+
tool_call_id=self.tool_call_id,
|
|
262
|
+
tool_name=self.tool_name,
|
|
263
|
+
content=self.content,
|
|
264
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Tool-related contracts: ToolSpec, ToolRegistry protocol, and CallBudget.
|
|
2
|
+
|
|
3
|
+
ToolSpec describes a tool's interface (name, description, parameters).
|
|
4
|
+
ToolRegistry is the protocol for registering and executing tools.
|
|
5
|
+
CallBudget tracks how many tool calls remain in a session.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .messages import Observation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ToolSpec:
|
|
19
|
+
"""Specification of a tool's interface.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: Unique tool name.
|
|
23
|
+
description: Human-readable description for the LLM.
|
|
24
|
+
parameters: JSON Schema dict describing the tool's arguments.
|
|
25
|
+
func: The callable that implements the tool.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
name: str
|
|
29
|
+
description: str
|
|
30
|
+
parameters: Dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
func: Any = None # callable, stored for execution
|
|
32
|
+
|
|
33
|
+
def to_openai_schema(self) -> Dict[str, Any]:
|
|
34
|
+
"""Convert to OpenAI-compatible tool schema format."""
|
|
35
|
+
return {
|
|
36
|
+
"type": "function",
|
|
37
|
+
"function": {
|
|
38
|
+
"name": self.name,
|
|
39
|
+
"description": self.description,
|
|
40
|
+
"parameters": self.parameters,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@runtime_checkable
|
|
46
|
+
class ToolRegistry(Protocol):
|
|
47
|
+
"""Protocol for tool registries.
|
|
48
|
+
|
|
49
|
+
A registry knows about available tools, can return their schemas,
|
|
50
|
+
and can execute a tool call by name.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def register(self, spec: ToolSpec) -> None:
|
|
54
|
+
"""Register a tool spec."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def get_schemas(self) -> List[Dict[str, Any]]:
|
|
58
|
+
"""Return OpenAI-compatible schemas for all registered tools."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def execute(
|
|
62
|
+
self,
|
|
63
|
+
tool_name: str,
|
|
64
|
+
arguments: Dict[str, Any],
|
|
65
|
+
tool_call_id: str = "",
|
|
66
|
+
) -> "Observation":
|
|
67
|
+
"""Execute a tool by name with the given arguments.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
An Observation with the result (or error).
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def tool_names(self) -> List[str]:
|
|
76
|
+
"""Return list of registered tool names."""
|
|
77
|
+
...
|
|
78
|
+
|
|
79
|
+
def __len__(self) -> int:
|
|
80
|
+
"""Number of registered tools."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class CallBudget:
|
|
86
|
+
"""Tracks remaining tool call budget.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
max_calls: Maximum number of tool calls allowed. None = unlimited.
|
|
90
|
+
used: Number of tool calls already consumed.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
max_calls: Optional[int] = None
|
|
94
|
+
used: int = 0
|
|
95
|
+
|
|
96
|
+
def consume(self, count: int = 1) -> bool:
|
|
97
|
+
"""Consume ``count`` calls. Returns True if within budget, False if exhausted."""
|
|
98
|
+
if self.max_calls is None:
|
|
99
|
+
self.used += count
|
|
100
|
+
return True
|
|
101
|
+
if self.used + count > self.max_calls:
|
|
102
|
+
# Still count the attempted calls so ``exhausted`` stays True
|
|
103
|
+
# but we report that we're over budget
|
|
104
|
+
self.used += count
|
|
105
|
+
return False
|
|
106
|
+
self.used += count
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def exhausted(self) -> bool:
|
|
111
|
+
"""Whether the budget has been fully consumed."""
|
|
112
|
+
if self.max_calls is None:
|
|
113
|
+
return False
|
|
114
|
+
return self.used >= self.max_calls
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def remaining(self) -> Optional[int]:
|
|
118
|
+
"""Number of remaining calls, or None if unlimited."""
|
|
119
|
+
if self.max_calls is None:
|
|
120
|
+
return None
|
|
121
|
+
return max(0, self.max_calls - self.used)
|
maque_core/factory.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Agent factory — convenient entry point for creating agents.
|
|
2
|
+
|
|
3
|
+
Provides ``create_agent`` which builds an :class:`Agent` from
|
|
4
|
+
explicit parameters, environment variables (via ``AppConfig.from_env``),
|
|
5
|
+
or a pre-built LLM provider.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .config import AppConfig
|
|
11
|
+
from .kernel.agent import Agent
|
|
12
|
+
from .llm.openai_compatible import OpenAICompatibleProvider
|
|
13
|
+
from .tools.discovery import discover_tools
|
|
14
|
+
from .tools.registry import ToolRegistry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_agent(
|
|
18
|
+
*,
|
|
19
|
+
model: str | None = None,
|
|
20
|
+
base_url: str | None = None,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
system_prompt: str | None = None,
|
|
23
|
+
max_iterations: int | None = None,
|
|
24
|
+
max_tool_calls: int | None = None,
|
|
25
|
+
max_history_turns: int | None = None,
|
|
26
|
+
include_reasoning_in_history: bool | None = None,
|
|
27
|
+
tools_package: str | list[str] | None = None,
|
|
28
|
+
llm=None,
|
|
29
|
+
) -> Agent:
|
|
30
|
+
"""Create an Agent from explicit parameters and environment defaults.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
model:
|
|
35
|
+
Model name. Overrides the environment default when provided.
|
|
36
|
+
base_url:
|
|
37
|
+
Base URL for the OpenAI-compatible endpoint.
|
|
38
|
+
api_key:
|
|
39
|
+
API key for the LLM endpoint.
|
|
40
|
+
system_prompt:
|
|
41
|
+
System prompt to prepend to the message history.
|
|
42
|
+
max_iterations:
|
|
43
|
+
Maximum ReAct iterations per ``ask()`` call.
|
|
44
|
+
max_tool_calls:
|
|
45
|
+
Maximum tool calls per ``ask()`` call.
|
|
46
|
+
max_history_turns:
|
|
47
|
+
Maximum conversation turns to keep in history.
|
|
48
|
+
include_reasoning_in_history:
|
|
49
|
+
Whether to include reasoning content in the message history.
|
|
50
|
+
tools_package:
|
|
51
|
+
Package name(s) to scan for ``@tool``-decorated functions.
|
|
52
|
+
``None`` means no tools are loaded by default.
|
|
53
|
+
A string loads one package; a list loads multiple packages
|
|
54
|
+
into the same registry.
|
|
55
|
+
llm:
|
|
56
|
+
Pre-built LLM provider. When provided, ``model``/``base_url``/
|
|
57
|
+
``api_key`` are ignored and this provider is used directly.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
Agent
|
|
62
|
+
A configured Agent instance ready to use.
|
|
63
|
+
"""
|
|
64
|
+
config = AppConfig.from_env()
|
|
65
|
+
|
|
66
|
+
# LLM
|
|
67
|
+
if llm is None:
|
|
68
|
+
llm = OpenAICompatibleProvider(
|
|
69
|
+
model=model if model is not None else config.llm.model,
|
|
70
|
+
base_url=base_url if base_url is not None else config.llm.base_url,
|
|
71
|
+
api_key=api_key if api_key is not None else config.llm.api_key,
|
|
72
|
+
temperature=config.llm.temperature,
|
|
73
|
+
timeout=config.llm.timeout,
|
|
74
|
+
max_tokens=config.llm.max_tokens,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Tools
|
|
78
|
+
registry = ToolRegistry()
|
|
79
|
+
if tools_package is not None:
|
|
80
|
+
if isinstance(tools_package, str):
|
|
81
|
+
discover_tools(registry, tools_package)
|
|
82
|
+
else:
|
|
83
|
+
for pkg in tools_package:
|
|
84
|
+
discover_tools(registry, pkg)
|
|
85
|
+
|
|
86
|
+
# Config overrides
|
|
87
|
+
resolved_system_prompt = system_prompt if system_prompt is not None else config.agent.system_prompt
|
|
88
|
+
resolved_max_iterations = max_iterations if max_iterations is not None else config.agent.max_iterations
|
|
89
|
+
resolved_max_tool_calls = max_tool_calls if max_tool_calls is not None else config.agent.max_tool_calls
|
|
90
|
+
resolved_max_history = max_history_turns if max_history_turns is not None else config.agent.max_history_turns
|
|
91
|
+
resolved_include_reasoning = (
|
|
92
|
+
include_reasoning_in_history
|
|
93
|
+
if include_reasoning_in_history is not None
|
|
94
|
+
else config.agent.include_reasoning_in_history
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return Agent(
|
|
98
|
+
llm=llm,
|
|
99
|
+
registry=registry,
|
|
100
|
+
system_prompt=resolved_system_prompt,
|
|
101
|
+
max_iterations=resolved_max_iterations,
|
|
102
|
+
max_tool_calls=resolved_max_tool_calls,
|
|
103
|
+
max_history_turns=resolved_max_history,
|
|
104
|
+
include_reasoning_in_history=resolved_include_reasoning,
|
|
105
|
+
)
|