maki-framework 0.3.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.
- maki/__init__.py +57 -0
- maki/__main__.py +104 -0
- maki/agents/__init__.py +27 -0
- maki/agents/agent.py +388 -0
- maki/agents/agent_manager.py +791 -0
- maki/agents/agents.py +16 -0
- maki/agents/approvals.py +209 -0
- maki/agents/plugin_handler.py +582 -0
- maki/agents/protocols.py +49 -0
- maki/agents/reasoning.py +267 -0
- maki/agents/verification.py +174 -0
- maki/agents/workflow.py +271 -0
- maki/backend.py +155 -0
- maki/config.py +123 -0
- maki/connector.py +446 -0
- maki/distributed/__init__.py +16 -0
- maki/distributed/circuit_breaker.py +117 -0
- maki/distributed/config_loader.py +135 -0
- maki/distributed/proxy.py +346 -0
- maki/distributed/registry.py +89 -0
- maki/distributed/server.py +254 -0
- maki/distributed/state_store.py +230 -0
- maki/exceptions.py +30 -0
- maki/logging_config.py +39 -0
- maki/makiAnthropic.py +384 -0
- maki/makiHG.py +237 -0
- maki/makiLLama.py +580 -0
- maki/makiLlamaCpp.py +274 -0
- maki/makiOpenAI.py +383 -0
- maki/makiOpenRouter.py +147 -0
- maki/objects.py +389 -0
- maki/plugins/__init__.py +47 -0
- maki/plugins/_web_utils.py +100 -0
- maki/plugins/alpaca_data/__init__.py +3 -0
- maki/plugins/alpaca_data/alpaca_data.py +271 -0
- maki/plugins/alpaca_data/example_usage.py +34 -0
- maki/plugins/alpaca_data/test_alpaca_data.py +141 -0
- maki/plugins/alpaca_news/__init__.py +3 -0
- maki/plugins/alpaca_news/alpaca_news.py +184 -0
- maki/plugins/alpaca_news/example_usage.py +37 -0
- maki/plugins/alpaca_news/test_alpaca_news.py +96 -0
- maki/plugins/alpaca_stream/__init__.py +3 -0
- maki/plugins/alpaca_stream/alpaca_stream.py +152 -0
- maki/plugins/alpaca_stream/example_usage.py +42 -0
- maki/plugins/alpaca_stream/test_alpaca_stream.py +84 -0
- maki/plugins/alpaca_trading/__init__.py +3 -0
- maki/plugins/alpaca_trading/alpaca_trading.py +189 -0
- maki/plugins/alpaca_trading/example_usage.py +44 -0
- maki/plugins/alpaca_trading/test_alpaca_trading.py +24 -0
- maki/plugins/directory_reader/__init__.py +10 -0
- maki/plugins/directory_reader/directory_reader.py +239 -0
- maki/plugins/directory_reader/example_usage.py +41 -0
- maki/plugins/directory_reader/test_directory_reader.py +94 -0
- maki/plugins/file_reader/__init__.py +10 -0
- maki/plugins/file_reader/example_usage.py +58 -0
- maki/plugins/file_reader/file_reader.py +272 -0
- maki/plugins/file_reader/test_file_reader.py +74 -0
- maki/plugins/file_writer/__init__.py +10 -0
- maki/plugins/file_writer/example_usage.py +47 -0
- maki/plugins/file_writer/file_writer.py +332 -0
- maki/plugins/file_writer/test_file_writer.py +96 -0
- maki/plugins/ftp_client/__init__.py +10 -0
- maki/plugins/ftp_client/example_usage.py +116 -0
- maki/plugins/ftp_client/ftp_client.py +845 -0
- maki/plugins/ftp_client/test_ftp_client.py +251 -0
- maki/plugins/image_classifier/__init__.py +11 -0
- maki/plugins/image_classifier/example_usage.py +83 -0
- maki/plugins/image_classifier/image_classifier.py +116 -0
- maki/plugins/image_classifier/test_image_classifier.py +182 -0
- maki/plugins/json_reader/__init__.py +7 -0
- maki/plugins/json_reader/example_usage.py +18 -0
- maki/plugins/json_reader/json_reader.py +113 -0
- maki/plugins/json_reader/test_json_reader.py +37 -0
- maki/plugins/obsidian_memory/__init__.py +3 -0
- maki/plugins/obsidian_memory/example_usage.py +54 -0
- maki/plugins/obsidian_memory/obsidian_memory.py +221 -0
- maki/plugins/obsidian_memory/test_obsidian_memory.py +83 -0
- maki/plugins/ocr/__init__.py +10 -0
- maki/plugins/ocr/backends/__init__.py +17 -0
- maki/plugins/ocr/backends/base.py +48 -0
- maki/plugins/ocr/backends/docx_backend.py +69 -0
- maki/plugins/ocr/backends/image_backend.py +49 -0
- maki/plugins/ocr/backends/llm_backend.py +164 -0
- maki/plugins/ocr/backends/pdf_backend.py +73 -0
- maki/plugins/ocr/backends/xlsx_backend.py +59 -0
- maki/plugins/ocr/example_usage.py +79 -0
- maki/plugins/ocr/ocr.py +242 -0
- maki/plugins/ocr/test_ocr.py +273 -0
- maki/plugins/postgres_store/__init__.py +10 -0
- maki/plugins/postgres_store/example_usage.py +35 -0
- maki/plugins/postgres_store/postgres_store.py +271 -0
- maki/plugins/postgres_store/test_postgres_store.py +133 -0
- maki/plugins/provider_updates/__init__.py +7 -0
- maki/plugins/provider_updates/example_usage.py +14 -0
- maki/plugins/provider_updates/provider_updates.py +101 -0
- maki/plugins/provider_updates/test_provider_updates.py +51 -0
- maki/plugins/rag_memory/__init__.py +3 -0
- maki/plugins/rag_memory/backends/__init__.py +57 -0
- maki/plugins/rag_memory/backends/base.py +70 -0
- maki/plugins/rag_memory/backends/chroma.py +95 -0
- maki/plugins/rag_memory/backends/faiss_local.py +123 -0
- maki/plugins/rag_memory/backends/memory_numpy.py +101 -0
- maki/plugins/rag_memory/backends/pgvector.py +137 -0
- maki/plugins/rag_memory/backends/qdrant.py +108 -0
- maki/plugins/rag_memory/example_usage.py +71 -0
- maki/plugins/rag_memory/rag_memory.py +383 -0
- maki/plugins/rag_memory/test_rag_memory.py +275 -0
- maki/plugins/trend_search/__init__.py +7 -0
- maki/plugins/trend_search/example_usage.py +14 -0
- maki/plugins/trend_search/test_trend_search.py +81 -0
- maki/plugins/trend_search/trend_search.py +115 -0
- maki/plugins/web_search/__init__.py +11 -0
- maki/plugins/web_search/example_usage.py +48 -0
- maki/plugins/web_search/test_web_search.py +539 -0
- maki/plugins/web_search/web_search.py +699 -0
- maki/plugins/web_to_md/__init__.py +9 -0
- maki/plugins/web_to_md/example_usage.py +47 -0
- maki/plugins/web_to_md/test_web_to_md.py +159 -0
- maki/plugins/web_to_md/web_to_md.py +296 -0
- maki/py.typed +0 -0
- maki/session.py +88 -0
- maki/structured.py +148 -0
- maki/test/__init__.py +3 -0
- maki/test/test_agent_fixes.py +567 -0
- maki/test/test_agent_functionality.py +177 -0
- maki/test/test_agent_proxy.py +478 -0
- maki/test/test_agent_server.py +313 -0
- maki/test/test_async_rate_limiter.py +341 -0
- maki/test/test_backend_hierarchy.py +327 -0
- maki/test/test_backend_interface.py +402 -0
- maki/test/test_concurrency.py +509 -0
- maki/test/test_config_loader.py +98 -0
- maki/test/test_connector.py +429 -0
- maki/test/test_consistency_sweep.py +349 -0
- maki/test/test_conversation_memory.py +398 -0
- maki/test/test_distributed_workflow.py +560 -0
- maki/test/test_enhanced_workflow.py +153 -0
- maki/test/test_error_handling.py +171 -0
- maki/test/test_hardening.py +503 -0
- maki/test/test_history_cleanup.py +114 -0
- maki/test/test_logging.py +32 -0
- maki/test/test_makiAnthropic.py +267 -0
- maki/test/test_makiLLama.py +297 -0
- maki/test/test_makiLLama_unittest.py +252 -0
- maki/test/test_makiLlamaCpp.py +291 -0
- maki/test/test_makiOpenAI.py +255 -0
- maki/test/test_makiOpenRouter.py +270 -0
- maki/test/test_mixin_contracts.py +384 -0
- maki/test/test_native_tools.py +652 -0
- maki/test/test_plugin_structure.py +50 -0
- maki/test/test_security.py +705 -0
- maki/test/test_serialization.py +313 -0
- maki/test/test_session.py +65 -0
- maki/test/test_ssrp_protection.py +105 -0
- maki/test/test_structured_output.py +128 -0
- maki/test/test_verification_types.py +97 -0
- maki/test/test_verified_workflow.py +281 -0
- maki/test/test_workflow_approval.py +205 -0
- maki/utils.py +250 -0
- maki_framework-0.3.0.dist-info/METADATA +582 -0
- maki_framework-0.3.0.dist-info/RECORD +165 -0
- maki_framework-0.3.0.dist-info/WHEEL +5 -0
- maki_framework-0.3.0.dist-info/entry_points.txt +3 -0
- maki_framework-0.3.0.dist-info/licenses/LICENSE +21 -0
- maki_framework-0.3.0.dist-info/top_level.txt +1 -0
maki/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public package interface for Maki.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
7
|
+
|
|
8
|
+
from . import config
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = _pkg_version("maki-framework")
|
|
12
|
+
except PackageNotFoundError: # pragma: no cover - running from source, not installed
|
|
13
|
+
__version__ = "0.0.0.dev0"
|
|
14
|
+
|
|
15
|
+
__all__ = ["LLMBackend", "Connector", "Utils", "Agent", "AgentManager", "MakiLLama",
|
|
16
|
+
"HFBackend", "MakiOpenAI", "MakiAnthropic", "MakiOpenRouter", "MakiLlamaCpp",
|
|
17
|
+
"LLMResponse",
|
|
18
|
+
"GenerationConfig", "Message", "ConversationMemory", "RateLimiter", "BackendType",
|
|
19
|
+
"ToolCall", "config", "__version__"]
|
|
20
|
+
|
|
21
|
+
_LAZY_EXPORTS = {
|
|
22
|
+
"LLMBackend": (".backend", "LLMBackend"),
|
|
23
|
+
"Connector": (".connector", "Connector"),
|
|
24
|
+
"Utils": (".utils", "Utils"),
|
|
25
|
+
"Agent": (".agents", "Agent"),
|
|
26
|
+
"AgentManager": (".agents", "AgentManager"),
|
|
27
|
+
"MakiLLama": (".makiLLama", "MakiLLama"),
|
|
28
|
+
"HFBackend": (".makiHG", "HFBackend"),
|
|
29
|
+
"MakiOpenAI": (".makiOpenAI", "MakiOpenAI"),
|
|
30
|
+
"MakiAnthropic": (".makiAnthropic", "MakiAnthropic"),
|
|
31
|
+
"MakiOpenRouter": (".makiOpenRouter", "MakiOpenRouter"),
|
|
32
|
+
"MakiLlamaCpp": (".makiLlamaCpp", "MakiLlamaCpp"),
|
|
33
|
+
"LLMResponse": (".objects", "LLMResponse"),
|
|
34
|
+
"GenerationConfig": (".objects", "GenerationConfig"),
|
|
35
|
+
"Message": (".objects", "Message"),
|
|
36
|
+
"ConversationMemory": (".objects", "ConversationMemory"),
|
|
37
|
+
"RateLimiter": (".objects", "RateLimiter"),
|
|
38
|
+
"BackendType": (".objects", "BackendType"),
|
|
39
|
+
"ToolCall": (".objects", "ToolCall"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __getattr__(name):
|
|
44
|
+
"""Load public exports only when they are requested."""
|
|
45
|
+
if name not in _LAZY_EXPORTS:
|
|
46
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
47
|
+
|
|
48
|
+
module_name, attribute_name = _LAZY_EXPORTS[name]
|
|
49
|
+
module = import_module(module_name, __name__)
|
|
50
|
+
value = getattr(module, attribute_name)
|
|
51
|
+
globals()[name] = value
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __dir__():
|
|
56
|
+
"""Expose lazy exports to introspection tools."""
|
|
57
|
+
return sorted(set(globals()) | set(__all__))
|
maki/__main__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for the Maki framework CLI.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
maki Print usage help.
|
|
6
|
+
maki serve --config <file> [options]
|
|
7
|
+
|
|
8
|
+
serve options:
|
|
9
|
+
--host HOST Bind address (default: 127.0.0.1)
|
|
10
|
+
--port PORT Listen port (default: 8100)
|
|
11
|
+
--api-key KEY Bearer token for auth
|
|
12
|
+
--tls-cert FILE TLS certificate file (enables HTTPS)
|
|
13
|
+
--tls-key FILE TLS private-key file
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from .config import DEFAULT_MODEL, DEFAULT_OLLAMA_BASE_URL
|
|
19
|
+
from .logging_config import configure_logging
|
|
20
|
+
from .makiLLama import MakiLLama
|
|
21
|
+
from .agents import Agent, AgentManager
|
|
22
|
+
|
|
23
|
+
__all__ = ['MakiLLama', 'Agent', 'AgentManager']
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _cmd_serve(args: argparse.Namespace) -> None:
|
|
27
|
+
if args.tls_key and not args.tls_cert:
|
|
28
|
+
print("Error: --tls-key requires --tls-cert", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
import uvicorn
|
|
33
|
+
except ImportError:
|
|
34
|
+
print("Error: uvicorn is required. Install with: pip install 'maki-framework[distributed]'",
|
|
35
|
+
file=sys.stderr)
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from .distributed.config_loader import load_agent_from_config
|
|
40
|
+
from .distributed.server import create_app
|
|
41
|
+
except ImportError as exc:
|
|
42
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
agent = load_agent_from_config(args.config)
|
|
47
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
48
|
+
print(f"Error loading config: {exc}", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
app = create_app(agent, api_key=args.api_key)
|
|
52
|
+
|
|
53
|
+
scheme = "https" if args.tls_cert else "http"
|
|
54
|
+
print(f"Maki Agent Server — {agent.name!r} ({agent.role or 'no role'})")
|
|
55
|
+
print(f" Listening on {scheme}://{args.host}:{args.port}")
|
|
56
|
+
print(f" Auth {'Bearer token' if args.api_key else 'disabled (trusted network)'}")
|
|
57
|
+
print(f" TLS {'enabled' if args.tls_cert else 'disabled'}")
|
|
58
|
+
print(f" Plugins {list(agent.plugins.keys()) or 'none'}")
|
|
59
|
+
|
|
60
|
+
uvicorn_kwargs: dict = {"host": args.host, "port": args.port}
|
|
61
|
+
if args.tls_cert:
|
|
62
|
+
uvicorn_kwargs["ssl_certfile"] = args.tls_cert
|
|
63
|
+
if args.tls_key:
|
|
64
|
+
uvicorn_kwargs["ssl_keyfile"] = args.tls_key
|
|
65
|
+
uvicorn.run(app, **uvicorn_kwargs)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> None:
|
|
69
|
+
parser = argparse.ArgumentParser(
|
|
70
|
+
prog="maki",
|
|
71
|
+
description="Maki framework — multi-agent LLM interactions",
|
|
72
|
+
)
|
|
73
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
74
|
+
|
|
75
|
+
# maki serve
|
|
76
|
+
serve = sub.add_parser("serve", help="Serve an agent over HTTP")
|
|
77
|
+
serve.add_argument("--config", required=True, metavar="FILE",
|
|
78
|
+
help="Path to agent YAML config file")
|
|
79
|
+
serve.add_argument("--host", default="127.0.0.1",
|
|
80
|
+
help="Bind host (default: 127.0.0.1)")
|
|
81
|
+
serve.add_argument("--port", type=int, default=8100,
|
|
82
|
+
help="Listen port (default: 8100)")
|
|
83
|
+
serve.add_argument("--api-key", default=None, metavar="KEY",
|
|
84
|
+
help="Bearer token for authentication (omit for open access)")
|
|
85
|
+
serve.add_argument("--tls-cert", default=None, metavar="FILE",
|
|
86
|
+
help="TLS certificate file — enables HTTPS when set")
|
|
87
|
+
serve.add_argument("--tls-key", default=None, metavar="FILE",
|
|
88
|
+
help="TLS private-key file (required when --tls-cert is set)")
|
|
89
|
+
|
|
90
|
+
args = parser.parse_args()
|
|
91
|
+
configure_logging()
|
|
92
|
+
|
|
93
|
+
if args.command == "serve":
|
|
94
|
+
_cmd_serve(args)
|
|
95
|
+
else:
|
|
96
|
+
parser.print_help()
|
|
97
|
+
print(f"\nExample:")
|
|
98
|
+
print(f" {DEFAULT_MODEL!r} via Ollama at {DEFAULT_OLLAMA_BASE_URL}")
|
|
99
|
+
print(f" from maki import MakiLLama")
|
|
100
|
+
print(f" from maki import Agent")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == "__main__":
|
|
104
|
+
main()
|
maki/agents/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent module for Maki Framework
|
|
3
|
+
|
|
4
|
+
This module provides the infrastructure for creating and managing multi-agent systems
|
|
5
|
+
using the Maki framework. Agents can work together to solve complex tasks through
|
|
6
|
+
coordination, delegation, and collaboration.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .agent import Agent
|
|
10
|
+
from .agent_manager import AgentManager
|
|
11
|
+
from .workflow import WorkflowTask, WorkflowState, TaskStatus, GateResult, TaskOutput
|
|
12
|
+
from .approvals import (
|
|
13
|
+
ApprovalSpec, ApprovalStore, ApprovalRecord, ApprovalDecision,
|
|
14
|
+
LocalApprovalStore, InMemoryApprovalStore,
|
|
15
|
+
)
|
|
16
|
+
from .verification import (
|
|
17
|
+
Verdict, VerifyResult, VerifySpec, AgentVerifier, agent_verifier,
|
|
18
|
+
)
|
|
19
|
+
from .protocols import PluginHostProtocol, ReasoningHostProtocol
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
'Agent', 'AgentManager', 'WorkflowTask', 'TaskStatus', 'WorkflowState',
|
|
23
|
+
'PluginHostProtocol', 'ReasoningHostProtocol',
|
|
24
|
+
'GateResult', 'TaskOutput', 'ApprovalSpec', 'ApprovalStore', 'ApprovalRecord', 'ApprovalDecision',
|
|
25
|
+
'LocalApprovalStore', 'InMemoryApprovalStore',
|
|
26
|
+
'Verdict', 'VerifyResult', 'VerifySpec', 'AgentVerifier', 'agent_verifier',
|
|
27
|
+
]
|
maki/agents/agent.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Agent class for the Maki Framework.
|
|
3
|
+
|
|
4
|
+
An Agent wraps a Maki LLM backend and provides task execution, memory,
|
|
5
|
+
stateful conversations, and extension points via the PluginHandler and
|
|
6
|
+
ReasoningEngine mixins.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections import deque
|
|
10
|
+
from typing import Dict, Any, Optional
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
from ..backend import LLMBackend
|
|
18
|
+
from ..exceptions import MakiError, MakiNetworkError, MakiTimeoutError, MakiAPIError
|
|
19
|
+
from ..objects import ConversationMemory, Message
|
|
20
|
+
from .plugin_handler import PluginHandler
|
|
21
|
+
from .reasoning import ReasoningEngine
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Errors that are worth retrying (transient network/timeout issues)
|
|
26
|
+
_RETRYABLE_ERRORS = (MakiNetworkError, MakiTimeoutError)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Agent(PluginHandler, ReasoningEngine):
|
|
30
|
+
"""An individual agent that can perform tasks using the Maki framework."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, name: str, maki_instance: LLMBackend, role: str = "", instructions: str = "",
|
|
33
|
+
stateful: bool = False, use_streaming: bool = False,
|
|
34
|
+
allow_dangerous_tools: bool = False):
|
|
35
|
+
"""
|
|
36
|
+
Initialize an agent.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name: Unique identifier for the agent
|
|
40
|
+
maki_instance: Maki instance to use for LLM interactions
|
|
41
|
+
role: The role of the agent (e.g., "researcher", "writer", "analyst")
|
|
42
|
+
instructions: Specific instructions for this agent
|
|
43
|
+
stateful: If True, prior task results are included in subsequent prompts
|
|
44
|
+
use_streaming: If True, execute_task uses streaming internally (chat_collect)
|
|
45
|
+
so the timeout applies per-chunk rather than to the full response.
|
|
46
|
+
Useful for tasks with very long outputs that exceed the backend timeout.
|
|
47
|
+
allow_dangerous_tools: If True, plugin methods marked in a plugin's
|
|
48
|
+
DANGEROUS_METHODS (file writes, uploads, deletes, …) may be
|
|
49
|
+
invoked via TOOL: directives. Off by default — a prompt-injected
|
|
50
|
+
instruction in scraped content should not be able to write
|
|
51
|
+
files or delete remote directories.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If name is not a valid string
|
|
55
|
+
TypeError: If maki_instance is not a Maki instance
|
|
56
|
+
"""
|
|
57
|
+
if not isinstance(name, str) or not name.strip():
|
|
58
|
+
raise ValueError("Agent name must be a non-empty string")
|
|
59
|
+
|
|
60
|
+
if not isinstance(role, str):
|
|
61
|
+
raise ValueError("Role must be a string")
|
|
62
|
+
|
|
63
|
+
if not isinstance(instructions, str):
|
|
64
|
+
raise ValueError("Instructions must be a string")
|
|
65
|
+
|
|
66
|
+
if not isinstance(maki_instance, LLMBackend) and not hasattr(maki_instance, 'request'):
|
|
67
|
+
raise TypeError("maki_instance must implement the LLMBackend interface")
|
|
68
|
+
|
|
69
|
+
self.agent_id = str(uuid.uuid4())
|
|
70
|
+
self.name = name.strip()
|
|
71
|
+
self.maki = maki_instance
|
|
72
|
+
self.role = role
|
|
73
|
+
self.instructions = instructions
|
|
74
|
+
self.stateful = stateful
|
|
75
|
+
self.use_streaming = use_streaming
|
|
76
|
+
self.allow_dangerous_tools = allow_dangerous_tools
|
|
77
|
+
self.memory = {}
|
|
78
|
+
|
|
79
|
+
# Maximum number of entries to keep in history; deque enforces this automatically
|
|
80
|
+
self._max_history_entries = 1000
|
|
81
|
+
self.reasoning_history: deque = deque(maxlen=self._max_history_entries)
|
|
82
|
+
self.task_history: deque = deque(maxlen=self._max_history_entries)
|
|
83
|
+
|
|
84
|
+
# Stateful multi-turn conversation memory (separate from task_history).
|
|
85
|
+
# Token-budgeted and eviction-based; replaces the old fixed-window + 300-char
|
|
86
|
+
# truncation approach.
|
|
87
|
+
self._conversation_memory = ConversationMemory(
|
|
88
|
+
max_entries=self._max_history_entries,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# One in-flight task at a time; acquired by execute_task / stream_task so
|
|
92
|
+
# concurrent callers (workflow thread pools, FastAPI handlers) are serialized.
|
|
93
|
+
self._execution_lock = threading.Lock()
|
|
94
|
+
|
|
95
|
+
# Validate and initialise mixin contracts (must come after all attrs are set).
|
|
96
|
+
self._init_reasoning()
|
|
97
|
+
self._init_plugins()
|
|
98
|
+
|
|
99
|
+
logger.info(f"Agent '{self.name}' initialized with role '{self.role}'")
|
|
100
|
+
|
|
101
|
+
def __repr__(self):
|
|
102
|
+
return f"Agent(name='{self.name}', role='{self.role}')"
|
|
103
|
+
|
|
104
|
+
def _build_history_section(self) -> str:
|
|
105
|
+
"""Return the prior-conversation block for stateful prompts, or empty string."""
|
|
106
|
+
if not self.stateful:
|
|
107
|
+
return ""
|
|
108
|
+
return self._conversation_memory.format_as_text()
|
|
109
|
+
|
|
110
|
+
def _build_system_message(self) -> str:
|
|
111
|
+
"""Return the system message (role + instructions) for this agent."""
|
|
112
|
+
return f"You are {self.name}, a {self.role}. {self.instructions}".strip()
|
|
113
|
+
|
|
114
|
+
def _build_user_message(self, task: str, context: Optional[Dict], use_plugins: bool) -> str:
|
|
115
|
+
"""Return the user message (task + optional context/plugins/history)."""
|
|
116
|
+
plugin_section = self.build_plugin_prompt_section() if use_plugins and self.plugins else ""
|
|
117
|
+
history_section = self._build_history_section()
|
|
118
|
+
parts = [task]
|
|
119
|
+
if context:
|
|
120
|
+
parts.append(f"Context: {json.dumps(context)}")
|
|
121
|
+
if plugin_section:
|
|
122
|
+
parts.append(plugin_section)
|
|
123
|
+
if history_section:
|
|
124
|
+
parts.append(history_section)
|
|
125
|
+
return "\n\n".join(p for p in parts if p)
|
|
126
|
+
|
|
127
|
+
def execute_task(self, task: str, context: Optional[Dict] = None, use_plugins: bool = False) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Execute a task using this agent.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
task: The task to perform
|
|
133
|
+
context: Additional context for the task
|
|
134
|
+
use_plugins: When True and plugins are loaded, their descriptions are included
|
|
135
|
+
in the prompt and the LLM may issue TOOL: calls that are executed before
|
|
136
|
+
the final response is synthesised.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The result of the task execution
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If task is not a valid string
|
|
143
|
+
MakiNetworkError: For network-related errors
|
|
144
|
+
MakiTimeoutError: For timeout errors
|
|
145
|
+
MakiAPIError: For API response errors
|
|
146
|
+
"""
|
|
147
|
+
if not isinstance(task, str) or not task.strip():
|
|
148
|
+
raise ValueError("Task must be a non-empty string")
|
|
149
|
+
|
|
150
|
+
with self._execution_lock:
|
|
151
|
+
try:
|
|
152
|
+
logger.debug(f"Executing task '{task}' for agent '{self.name}'")
|
|
153
|
+
start_time = time.time()
|
|
154
|
+
system_msg = self._build_system_message()
|
|
155
|
+
|
|
156
|
+
if use_plugins and self.plugins and getattr(self.maki, "supports_native_tools", False) is True:
|
|
157
|
+
# Native tool-calling path: backend drives structured tool calls.
|
|
158
|
+
result = self.execute_with_native_tools(task, context, system_msg)
|
|
159
|
+
else:
|
|
160
|
+
user_msg = self._build_user_message(task, context, use_plugins)
|
|
161
|
+
if self.use_streaming:
|
|
162
|
+
result = self.maki.chat_collect(user_msg, system=system_msg).content
|
|
163
|
+
else:
|
|
164
|
+
result = self.maki.chat(user_msg, system=system_msg).content
|
|
165
|
+
if result is None:
|
|
166
|
+
raise MakiAPIError(f"Backend returned None content for task '{task}'")
|
|
167
|
+
|
|
168
|
+
# Legacy TOOL: directive path for non-native backends.
|
|
169
|
+
if use_plugins and self.plugins:
|
|
170
|
+
result = self.handle_plugin_calls(result, task, context)
|
|
171
|
+
|
|
172
|
+
execution_time = time.time() - start_time
|
|
173
|
+
logger.debug(f"Task '{task}' completed in {execution_time:.2f}s for agent '{self.name}'")
|
|
174
|
+
except (MakiError, ValueError, TypeError) as e:
|
|
175
|
+
logger.error(f"Failed to execute task '{task}' for agent '{self.name}': {str(e)}", exc_info=True)
|
|
176
|
+
raise
|
|
177
|
+
except Exception as e:
|
|
178
|
+
logger.error(f"Unexpected error executing task '{task}' for agent '{self.name}': {str(e)}", exc_info=True)
|
|
179
|
+
raise MakiError(f"Failed to execute task '{task}' for agent '{self.name}': {str(e)}") from e
|
|
180
|
+
|
|
181
|
+
# Record the task execution in history
|
|
182
|
+
self.task_history.append({
|
|
183
|
+
'task': task,
|
|
184
|
+
'context': context,
|
|
185
|
+
'result': result,
|
|
186
|
+
'timestamp': time.time()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
# Maintain stateful conversation history
|
|
190
|
+
if self.stateful:
|
|
191
|
+
self._conversation_memory.append(Message("user", task))
|
|
192
|
+
self._conversation_memory.append(Message("assistant", result))
|
|
193
|
+
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
def reset_conversation(self):
|
|
197
|
+
"""Clear the stateful conversation history."""
|
|
198
|
+
self._conversation_memory.clear()
|
|
199
|
+
|
|
200
|
+
def execute_task_with_retry(self, task: str, context: Optional[Dict] = None,
|
|
201
|
+
max_retries: int = 3, retry_delay: float = 1.0,
|
|
202
|
+
use_plugins: bool = False) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Execute a task with retry logic.
|
|
205
|
+
|
|
206
|
+
Only transient network/timeout errors are retried. Non-retryable errors
|
|
207
|
+
(MakiAPIError, ValueError, etc.) are re-raised immediately without waiting.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
task: The task to perform
|
|
211
|
+
context: Additional context for the task
|
|
212
|
+
max_retries: Maximum number of retry attempts
|
|
213
|
+
retry_delay: Delay between retries in seconds
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
The result of the task execution
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
Exception: If task fails after all retries (transient) or immediately (non-transient)
|
|
220
|
+
"""
|
|
221
|
+
for attempt in range(max_retries):
|
|
222
|
+
try:
|
|
223
|
+
return self.execute_task(task, context, use_plugins=use_plugins)
|
|
224
|
+
except _RETRYABLE_ERRORS as e:
|
|
225
|
+
if attempt == max_retries - 1:
|
|
226
|
+
raise
|
|
227
|
+
logger.warning(
|
|
228
|
+
f"Task '{task}' failed on attempt {attempt + 1}/{max_retries}: "
|
|
229
|
+
f"{str(e)}. Retrying in {retry_delay}s..."
|
|
230
|
+
)
|
|
231
|
+
time.sleep(retry_delay)
|
|
232
|
+
except Exception:
|
|
233
|
+
# Non-retryable (API error, bad input, etc.) -- fail fast
|
|
234
|
+
raise
|
|
235
|
+
|
|
236
|
+
# Unreachable, but satisfies type checkers
|
|
237
|
+
raise RuntimeError(f"Task '{task}' failed after {max_retries} attempts")
|
|
238
|
+
|
|
239
|
+
def stream_task(self, task: str, context: Optional[Dict] = None, use_plugins: bool = False):
|
|
240
|
+
"""
|
|
241
|
+
Stream a task response token by token.
|
|
242
|
+
|
|
243
|
+
Requires a backend that supports streaming (e.g., MakiLLama). Raises
|
|
244
|
+
NotImplementedError if the configured backend has no stream() method.
|
|
245
|
+
|
|
246
|
+
History (task_history and stateful conversation history) is recorded
|
|
247
|
+
via a finally block in the returned generator, so it is always updated
|
|
248
|
+
regardless of whether the caller consumes the stream fully or abandons
|
|
249
|
+
it early.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
task: The task to perform
|
|
253
|
+
context: Additional context for the task
|
|
254
|
+
use_plugins: When True and plugins are loaded, plugin descriptions
|
|
255
|
+
are included in the prompt. Note: TOOL: directives emitted
|
|
256
|
+
during streaming are not executed mid-stream; use execute_task()
|
|
257
|
+
if you need plugin call/response cycles.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
A generator that yields response chunks
|
|
261
|
+
"""
|
|
262
|
+
if not isinstance(task, str) or not task.strip():
|
|
263
|
+
raise ValueError("Task must be a non-empty string")
|
|
264
|
+
|
|
265
|
+
system_msg = self._build_system_message()
|
|
266
|
+
user_msg = self._build_user_message(task, context, use_plugins)
|
|
267
|
+
stream_kwargs = {"system": system_msg}
|
|
268
|
+
|
|
269
|
+
def _tracked():
|
|
270
|
+
chunks = []
|
|
271
|
+
with self._execution_lock:
|
|
272
|
+
try:
|
|
273
|
+
raw_stream = self.maki.stream(user_msg, **stream_kwargs)
|
|
274
|
+
except NotImplementedError as e:
|
|
275
|
+
raise NotImplementedError(
|
|
276
|
+
f"Backend '{type(self.maki).__name__}' does not support streaming. "
|
|
277
|
+
"Use MakiLLama or another streaming-capable backend instead."
|
|
278
|
+
) from e
|
|
279
|
+
try:
|
|
280
|
+
for chunk in raw_stream:
|
|
281
|
+
chunks.append(chunk)
|
|
282
|
+
yield chunk
|
|
283
|
+
finally:
|
|
284
|
+
# Always record whatever was produced, even on early abandonment.
|
|
285
|
+
if chunks:
|
|
286
|
+
full_result = "".join(chunks)
|
|
287
|
+
self.task_history.append({
|
|
288
|
+
'task': task,
|
|
289
|
+
'context': context,
|
|
290
|
+
'result': full_result,
|
|
291
|
+
'timestamp': time.time(),
|
|
292
|
+
})
|
|
293
|
+
if self.stateful:
|
|
294
|
+
self._conversation_memory.append(Message("user", task))
|
|
295
|
+
self._conversation_memory.append(Message("assistant", full_result))
|
|
296
|
+
|
|
297
|
+
return _tracked()
|
|
298
|
+
|
|
299
|
+
def remember(self, key: str, value: Any):
|
|
300
|
+
"""Store information in the agent's memory."""
|
|
301
|
+
self.memory[key] = value
|
|
302
|
+
|
|
303
|
+
def recall(self, key: str) -> Any:
|
|
304
|
+
"""Retrieve information from the agent's memory."""
|
|
305
|
+
return self.memory.get(key, None)
|
|
306
|
+
|
|
307
|
+
def clear_memory(self):
|
|
308
|
+
"""Clear the agent's memory."""
|
|
309
|
+
self.memory.clear()
|
|
310
|
+
|
|
311
|
+
def to_dict(self) -> dict:
|
|
312
|
+
"""Serialize the agent's state to a JSON-compatible dict.
|
|
313
|
+
|
|
314
|
+
The LLM backend is not included — pass it explicitly to from_dict().
|
|
315
|
+
"""
|
|
316
|
+
return {
|
|
317
|
+
'agent_id': self.agent_id,
|
|
318
|
+
'name': self.name,
|
|
319
|
+
'role': self.role,
|
|
320
|
+
'instructions': self.instructions,
|
|
321
|
+
'stateful': self.stateful,
|
|
322
|
+
'use_streaming': self.use_streaming,
|
|
323
|
+
'memory': dict(self.memory),
|
|
324
|
+
'task_history': list(self.task_history),
|
|
325
|
+
'conversation_memory': self._conversation_memory.to_list(),
|
|
326
|
+
'conversation_token_budget': self._conversation_memory.token_budget,
|
|
327
|
+
'max_history_entries': self._max_history_entries,
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
@classmethod
|
|
331
|
+
def from_dict(cls, data: dict, maki_instance: LLMBackend) -> 'Agent':
|
|
332
|
+
"""Reconstruct an Agent from a dict produced by to_dict().
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
data: Dict previously returned by to_dict().
|
|
336
|
+
maki_instance: LLM backend to attach (not serialized).
|
|
337
|
+
"""
|
|
338
|
+
agent = cls(
|
|
339
|
+
name=data['name'],
|
|
340
|
+
maki_instance=maki_instance,
|
|
341
|
+
role=data.get('role', ''),
|
|
342
|
+
instructions=data.get('instructions', ''),
|
|
343
|
+
stateful=data.get('stateful', False),
|
|
344
|
+
use_streaming=data.get('use_streaming', False),
|
|
345
|
+
)
|
|
346
|
+
agent.agent_id = data['agent_id']
|
|
347
|
+
agent.memory = dict(data.get('memory', {}))
|
|
348
|
+
max_entries = data.get('max_history_entries', 1000)
|
|
349
|
+
agent._max_history_entries = max_entries
|
|
350
|
+
agent.task_history = deque(data.get('task_history', []), maxlen=max_entries)
|
|
351
|
+
token_budget = data.get('conversation_token_budget', ConversationMemory.DEFAULT_TOKEN_BUDGET)
|
|
352
|
+
if 'conversation_memory' in data:
|
|
353
|
+
agent._conversation_memory = ConversationMemory.from_list(
|
|
354
|
+
data['conversation_memory'],
|
|
355
|
+
token_budget=token_budget,
|
|
356
|
+
max_entries=max_entries,
|
|
357
|
+
)
|
|
358
|
+
elif 'conversation_history' in data:
|
|
359
|
+
# Migration from old {'task': ..., 'result': ...} format
|
|
360
|
+
agent._conversation_memory = ConversationMemory(
|
|
361
|
+
token_budget=token_budget, max_entries=max_entries,
|
|
362
|
+
)
|
|
363
|
+
for turn in data['conversation_history']:
|
|
364
|
+
agent._conversation_memory._messages.append(Message('user', turn.get('task', '')))
|
|
365
|
+
agent._conversation_memory._messages.append(Message('assistant', turn.get('result', '')))
|
|
366
|
+
else:
|
|
367
|
+
agent._conversation_memory = ConversationMemory(
|
|
368
|
+
token_budget=token_budget, max_entries=max_entries,
|
|
369
|
+
)
|
|
370
|
+
return agent
|
|
371
|
+
|
|
372
|
+
def set_max_history_entries(self, max_entries: int):
|
|
373
|
+
"""Set the maximum number of entries to keep in history.
|
|
374
|
+
|
|
375
|
+
Existing history is preserved up to the new limit (most recent entries kept).
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
max_entries: Maximum number of entries to keep in history
|
|
379
|
+
"""
|
|
380
|
+
if max_entries <= 0:
|
|
381
|
+
raise ValueError("max_entries must be positive")
|
|
382
|
+
self._max_history_entries = max_entries
|
|
383
|
+
# Recreate deques with new maxlen, preserving most recent entries
|
|
384
|
+
self.reasoning_history = deque(self.reasoning_history, maxlen=max_entries)
|
|
385
|
+
self.task_history = deque(self.task_history, maxlen=max_entries)
|
|
386
|
+
# max_entries setter trims immediately if the new cap is smaller
|
|
387
|
+
self._conversation_memory.max_entries = max(2, max_entries)
|
|
388
|
+
|