inquirycraft 0.6.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.
- inquirycraft/__init__.py +76 -0
- inquirycraft/__main__.py +4 -0
- inquirycraft/adapters.py +127 -0
- inquirycraft/agent/__init__.py +5 -0
- inquirycraft/agent/base.py +194 -0
- inquirycraft/cli/__init__.py +13 -0
- inquirycraft/cli/app.py +231 -0
- inquirycraft/cli/contracts.py +46 -0
- inquirycraft/cli/factory.py +33 -0
- inquirycraft/events/__init__.py +25 -0
- inquirycraft/events/models.py +70 -0
- inquirycraft/events/replay.py +307 -0
- inquirycraft/events/sink.py +231 -0
- inquirycraft/executor/__init__.py +15 -0
- inquirycraft/executor/base.py +88 -0
- inquirycraft/executor/dep_utils.py +40 -0
- inquirycraft/executor/output.py +167 -0
- inquirycraft/executor/python.py +129 -0
- inquirycraft/executor/shell.py +65 -0
- inquirycraft/llm/__init__.py +42 -0
- inquirycraft/llm/base.py +288 -0
- inquirycraft/llm/errors.py +16 -0
- inquirycraft/llm/factory.py +95 -0
- inquirycraft/llm/logprob_stream.py +160 -0
- inquirycraft/llm/message_format.py +81 -0
- inquirycraft/llm/models.py +42 -0
- inquirycraft/llm/online.py +71 -0
- inquirycraft/llm/online_support.py +99 -0
- inquirycraft/llm/openai_adapter.py +109 -0
- inquirycraft/llm/pool.py +142 -0
- inquirycraft/llm/pooled.py +344 -0
- inquirycraft/llm/protocol.py +15 -0
- inquirycraft/llm/public_requests.py +123 -0
- inquirycraft/llm/request_client.py +230 -0
- inquirycraft/llm/retry.py +50 -0
- inquirycraft/llm/stream_types.py +123 -0
- inquirycraft/llm/text_stream.py +154 -0
- inquirycraft/llm/tool_args.py +134 -0
- inquirycraft/llm/tool_requests.py +194 -0
- inquirycraft/llm/tool_stream_result.py +90 -0
- inquirycraft/llm/tool_stream_state.py +187 -0
- inquirycraft/llm/usage.py +55 -0
- inquirycraft/llm/utils.py +288 -0
- inquirycraft/mcp/__init__.py +8 -0
- inquirycraft/mcp/client.py +118 -0
- inquirycraft/memory/__init__.py +83 -0
- inquirycraft/memory/context.py +63 -0
- inquirycraft/memory/conversation.py +83 -0
- inquirycraft/memory/history.py +184 -0
- inquirycraft/memory/kv_storage.py +69 -0
- inquirycraft/memory/legacy_records.py +27 -0
- inquirycraft/memory/message.py +260 -0
- inquirycraft/memory/mutation.py +67 -0
- inquirycraft/memory/paths.py +19 -0
- inquirycraft/memory/record_store.py +218 -0
- inquirycraft/memory/records.py +64 -0
- inquirycraft/runtime/__init__.py +42 -0
- inquirycraft/runtime/cancellation.py +24 -0
- inquirycraft/runtime/engine.py +313 -0
- inquirycraft/runtime/hooks.py +97 -0
- inquirycraft/runtime/hosted.py +114 -0
- inquirycraft/runtime/resume.py +172 -0
- inquirycraft/runtime/state.py +15 -0
- inquirycraft/runtime/streaming.py +95 -0
- inquirycraft/runtime/subprocess.py +298 -0
- inquirycraft/runtime/subprocess_sync.py +122 -0
- inquirycraft/runtime/telemetry.py +87 -0
- inquirycraft/tools/__init__.py +136 -0
- inquirycraft/tools/artifacts/__init__.py +11 -0
- inquirycraft/tools/artifacts/reducers.py +41 -0
- inquirycraft/tools/artifacts/store.py +146 -0
- inquirycraft/tools/base.py +188 -0
- inquirycraft/tools/builtin.py +101 -0
- inquirycraft/tools/collection.py +100 -0
- inquirycraft/tools/dispatch.py +75 -0
- inquirycraft/tools/execution/__init__.py +7 -0
- inquirycraft/tools/execution/engine.py +105 -0
- inquirycraft/tools/execution/middleware.py +30 -0
- inquirycraft/tools/execution/models.py +19 -0
- inquirycraft/tools/executor.py +54 -0
- inquirycraft/tools/file_tools.py +269 -0
- inquirycraft/tools/filesystem/__init__.py +65 -0
- inquirycraft/tools/filesystem/edit.py +263 -0
- inquirycraft/tools/filesystem/modify.py +168 -0
- inquirycraft/tools/filesystem/observe.py +323 -0
- inquirycraft/tools/filesystem/search.py +205 -0
- inquirycraft/tools/guards.py +160 -0
- inquirycraft/tools/path_policy.py +277 -0
- inquirycraft/tools/search_tools.py +190 -0
- inquirycraft/tools/shell_feedback.py +65 -0
- inquirycraft/tools/shell_guards.py +51 -0
- inquirycraft/tools/shell_output.py +109 -0
- inquirycraft/tools/shell_reducer.py +198 -0
- inquirycraft/tools/shell_traceback.py +220 -0
- inquirycraft/tools/tool_calls.py +51 -0
- inquirycraft/tools/write_safety.py +90 -0
- inquirycraft-0.6.0.dist-info/METADATA +222 -0
- inquirycraft-0.6.0.dist-info/RECORD +102 -0
- inquirycraft-0.6.0.dist-info/WHEEL +5 -0
- inquirycraft-0.6.0.dist-info/entry_points.txt +2 -0
- inquirycraft-0.6.0.dist-info/licenses/LICENSE +9 -0
- inquirycraft-0.6.0.dist-info/top_level.txt +1 -0
inquirycraft/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""InquiryCraft's lightweight, domain-independent agent runtime.
|
|
2
|
+
|
|
3
|
+
Every public symbol is resolved lazily. Importing the package does not initialize
|
|
4
|
+
the runtime, CLI, or provider adapters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("inquirycraft")
|
|
13
|
+
except PackageNotFoundError: # source checkout without an installed distribution
|
|
14
|
+
__version__ = "0+unknown"
|
|
15
|
+
|
|
16
|
+
_EXPORTS = {
|
|
17
|
+
"AgentHooks": ("inquirycraft.runtime", "AgentHooks"),
|
|
18
|
+
"AgentRuntime": ("inquirycraft.runtime", "AgentRuntime"),
|
|
19
|
+
"BaseTool": ("inquirycraft.tools", "BaseTool"),
|
|
20
|
+
"CompletionRequest": ("inquirycraft.llm", "CompletionRequest"),
|
|
21
|
+
"CompletionResult": ("inquirycraft.llm", "CompletionResult"),
|
|
22
|
+
"CompositeEventSink": ("inquirycraft.events", "CompositeEventSink"),
|
|
23
|
+
"Conversation": ("inquirycraft.memory", "Conversation"),
|
|
24
|
+
"EventSink": ("inquirycraft.events", "EventSink"),
|
|
25
|
+
"JsonlEventSink": ("inquirycraft.events", "JsonlEventSink"),
|
|
26
|
+
"LLMClient": ("inquirycraft.llm", "LLMClient"),
|
|
27
|
+
"Message": ("inquirycraft.memory", "Message"),
|
|
28
|
+
"RecordingLLMClient": ("inquirycraft.events", "RecordingLLMClient"),
|
|
29
|
+
"ReplayLLMClient": ("inquirycraft.events", "ReplayLLMClient"),
|
|
30
|
+
"ResumeState": ("inquirycraft.runtime", "ResumeState"),
|
|
31
|
+
"RuntimeContext": ("inquirycraft.runtime", "RuntimeContext"),
|
|
32
|
+
"RuntimeEvent": ("inquirycraft.events", "RuntimeEvent"),
|
|
33
|
+
"RuntimeOptions": ("inquirycraft.runtime", "RuntimeOptions"),
|
|
34
|
+
"ToolCollection": ("inquirycraft.tools", "ToolCollection"),
|
|
35
|
+
"ToolContext": ("inquirycraft.tools", "ToolContext"),
|
|
36
|
+
"ToolResult": ("inquirycraft.tools", "ToolResult"),
|
|
37
|
+
"Usage": ("inquirycraft.llm", "Usage"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"__version__",
|
|
42
|
+
"AgentHooks",
|
|
43
|
+
"AgentRuntime",
|
|
44
|
+
"BaseTool",
|
|
45
|
+
"CompletionRequest",
|
|
46
|
+
"CompletionResult",
|
|
47
|
+
"CompositeEventSink",
|
|
48
|
+
"Conversation",
|
|
49
|
+
"EventSink",
|
|
50
|
+
"JsonlEventSink",
|
|
51
|
+
"LLMClient",
|
|
52
|
+
"Message",
|
|
53
|
+
"RecordingLLMClient",
|
|
54
|
+
"ReplayLLMClient",
|
|
55
|
+
"ResumeState",
|
|
56
|
+
"RuntimeContext",
|
|
57
|
+
"RuntimeEvent",
|
|
58
|
+
"RuntimeOptions",
|
|
59
|
+
"ToolCollection",
|
|
60
|
+
"ToolContext",
|
|
61
|
+
"ToolResult",
|
|
62
|
+
"Usage",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name: str) -> Any:
|
|
67
|
+
if name not in _EXPORTS:
|
|
68
|
+
raise AttributeError(name)
|
|
69
|
+
module_name, attribute = _EXPORTS[name]
|
|
70
|
+
value = getattr(import_module(module_name), attribute)
|
|
71
|
+
globals()[name] = value
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def __dir__() -> list[str]:
|
|
76
|
+
return sorted(set(globals()) | set(__all__))
|
inquirycraft/__main__.py
ADDED
inquirycraft/adapters.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from inquirycraft.llm import CompletionRequest, CompletionResult, StreamChunk, Usage
|
|
7
|
+
from inquirycraft.memory import Message
|
|
8
|
+
from inquirycraft.tools import BaseTool, ToolContext, ToolResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def legacy_message_to_message(value: Any) -> Message:
|
|
12
|
+
"""Convert a legacy Message or OpenAI-style dict without importing its package."""
|
|
13
|
+
if isinstance(value, Mapping):
|
|
14
|
+
row = dict(value)
|
|
15
|
+
elif hasattr(value, "to_dict"):
|
|
16
|
+
row = value.to_dict()
|
|
17
|
+
elif hasattr(value, "model_dump"):
|
|
18
|
+
row = value.model_dump(exclude_none=True)
|
|
19
|
+
else:
|
|
20
|
+
raise TypeError(f"Unsupported legacy message: {type(value)!r}")
|
|
21
|
+
return Message.from_dict(row)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LegacyAskLLMAdapter:
|
|
25
|
+
"""Expose an existing ask()/ask_tool()-compatible client as ``LLMClient``.
|
|
26
|
+
|
|
27
|
+
This adapter lets hosts adopt the runtime contract while retaining an existing
|
|
28
|
+
provider implementation, retry policy, and request parameters.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, legacy_llm: Any) -> None:
|
|
32
|
+
self.legacy_llm = legacy_llm
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _usage(legacy_llm: Any) -> Usage:
|
|
36
|
+
return Usage(
|
|
37
|
+
input_tokens=int(getattr(legacy_llm, "_last_call_input_tokens", 0) or 0),
|
|
38
|
+
output_tokens=int(getattr(legacy_llm, "_last_call_output_tokens", 0) or 0),
|
|
39
|
+
cached_input_tokens=int(getattr(legacy_llm, "_last_call_input_cached_tokens", 0) or 0),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def complete(self, request: CompletionRequest) -> CompletionResult:
|
|
43
|
+
messages = [dict(message) for message in request.messages]
|
|
44
|
+
if request.tools:
|
|
45
|
+
response = await self.legacy_llm.ask_tool(
|
|
46
|
+
messages=messages,
|
|
47
|
+
system_msgs=None,
|
|
48
|
+
timeout=int(request.timeout or 300),
|
|
49
|
+
tools=[dict(tool) for tool in request.tools],
|
|
50
|
+
tool_choice=request.tool_choice,
|
|
51
|
+
temperature=request.temperature,
|
|
52
|
+
)
|
|
53
|
+
raw_calls = getattr(response, "tool_calls", None) or ()
|
|
54
|
+
tool_calls = tuple(
|
|
55
|
+
call.model_dump(exclude_none=True) if hasattr(call, "model_dump") else dict(call)
|
|
56
|
+
for call in raw_calls
|
|
57
|
+
)
|
|
58
|
+
return CompletionResult(
|
|
59
|
+
content=str(getattr(response, "content", "") or ""),
|
|
60
|
+
reasoning_content=str(getattr(response, "reasoning_content", "") or ""),
|
|
61
|
+
tool_calls=tool_calls,
|
|
62
|
+
finish_reason=str(getattr(self.legacy_llm, "_last_finish_reason", "unknown")),
|
|
63
|
+
usage=self._usage(self.legacy_llm),
|
|
64
|
+
raw=response,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
content = await self.legacy_llm.ask(
|
|
68
|
+
messages=messages,
|
|
69
|
+
system_msgs=None,
|
|
70
|
+
stream=False,
|
|
71
|
+
temperature=request.temperature,
|
|
72
|
+
timeout=request.timeout,
|
|
73
|
+
)
|
|
74
|
+
return CompletionResult(
|
|
75
|
+
content=str(content or ""),
|
|
76
|
+
finish_reason=str(getattr(self.legacy_llm, "_last_finish_reason", "unknown")),
|
|
77
|
+
usage=self._usage(self.legacy_llm),
|
|
78
|
+
raw=content,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
async def stream(self, request: CompletionRequest) -> AsyncIterator[StreamChunk]:
|
|
82
|
+
result = await self.complete(request)
|
|
83
|
+
yield StreamChunk(
|
|
84
|
+
content=result.content,
|
|
85
|
+
reasoning_content=result.reasoning_content,
|
|
86
|
+
finish_reason=result.finish_reason,
|
|
87
|
+
usage=result.usage,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def aclose(self) -> None:
|
|
91
|
+
instances = getattr(self.legacy_llm, "_instances", None)
|
|
92
|
+
targets = instances if isinstance(instances, list) else [self.legacy_llm]
|
|
93
|
+
for target in targets:
|
|
94
|
+
client = getattr(target, "client", None)
|
|
95
|
+
close = getattr(client, "close", None)
|
|
96
|
+
if close is not None:
|
|
97
|
+
result = close()
|
|
98
|
+
if hasattr(result, "__await__"):
|
|
99
|
+
await result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class LegacyToolAdapter(BaseTool):
|
|
103
|
+
"""Expose a legacy BaseTool through the context-aware contract."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, legacy_tool: Any) -> None:
|
|
106
|
+
self.legacy_tool = legacy_tool
|
|
107
|
+
self.name = str(legacy_tool.name)
|
|
108
|
+
self.description = str(legacy_tool.description)
|
|
109
|
+
self.input_schema = dict(getattr(legacy_tool, "parameters", None) or {})
|
|
110
|
+
|
|
111
|
+
async def execute(self, arguments: Mapping[str, Any], context: ToolContext) -> ToolResult:
|
|
112
|
+
context.cancellation.raise_if_cancelled()
|
|
113
|
+
raw = await self.legacy_tool.execute(**dict(arguments))
|
|
114
|
+
if hasattr(raw, "output") or hasattr(raw, "error"):
|
|
115
|
+
output = getattr(raw, "output", "")
|
|
116
|
+
error = getattr(raw, "error", None)
|
|
117
|
+
system = getattr(raw, "system", None)
|
|
118
|
+
metadata = {"legacy_system": system} if system is not None else {}
|
|
119
|
+
return ToolResult(
|
|
120
|
+
output=str(output or ""),
|
|
121
|
+
error=str(error) if error else None,
|
|
122
|
+
metadata=metadata,
|
|
123
|
+
)
|
|
124
|
+
return ToolResult(output=str(raw or ""))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = ["LegacyAskLLMAdapter", "LegacyToolAdapter", "legacy_message_to_message"]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
10
|
+
|
|
11
|
+
from inquirycraft.memory import Memory
|
|
12
|
+
from inquirycraft.memory.message import ROLE_TYPE, Message, Role
|
|
13
|
+
from inquirycraft.runtime import AgentState
|
|
14
|
+
from inquirycraft.tools import (
|
|
15
|
+
TOOL_CHOICE_TYPE,
|
|
16
|
+
ToolCall,
|
|
17
|
+
ToolChoice,
|
|
18
|
+
ToolCollection,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MemoryRetrievalCompatibilityMixin(ABC):
|
|
25
|
+
"""Stable fields for the removed vector-memory integration."""
|
|
26
|
+
|
|
27
|
+
vector_db_path: str = Field(default="./vector_db", description="Vector database path")
|
|
28
|
+
model_name: str = Field(
|
|
29
|
+
default="./intfloat/multilingual-e5-large-instruct",
|
|
30
|
+
description="Encoding model path",
|
|
31
|
+
)
|
|
32
|
+
use_memory_retrieval: bool = Field(default=False)
|
|
33
|
+
retrieval_top_k: int = Field(default=3)
|
|
34
|
+
min_common_length: int = Field(default=100)
|
|
35
|
+
save_to_vector_db: bool = Field(default=False)
|
|
36
|
+
fusion_retrieval: bool = Field(default=False)
|
|
37
|
+
collection_name: str = Field(default="conversations")
|
|
38
|
+
|
|
39
|
+
def _initialize_retrieval_components(self) -> None:
|
|
40
|
+
if self.use_memory_retrieval or self.save_to_vector_db:
|
|
41
|
+
raise RuntimeError("InquiryCraft vector memory retrieval has been removed")
|
|
42
|
+
|
|
43
|
+
async def _retrieve_similar_memories(
|
|
44
|
+
self,
|
|
45
|
+
query: str,
|
|
46
|
+
retrieval_filter: dict[str, Any] | None = None,
|
|
47
|
+
) -> list[dict[str, Any]]:
|
|
48
|
+
_ = query, retrieval_filter
|
|
49
|
+
self._initialize_retrieval_components()
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
async def _save_to_vector_db(
|
|
53
|
+
self,
|
|
54
|
+
text: str,
|
|
55
|
+
metadata: dict[str, Any],
|
|
56
|
+
doc_id: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
_ = text, metadata, doc_id
|
|
59
|
+
self._initialize_retrieval_components()
|
|
60
|
+
|
|
61
|
+
def get_vector_db_stats(self) -> dict[str, Any]:
|
|
62
|
+
return {"enabled": False, "removed": True}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class BaseAgent(BaseModel, MemoryRetrievalCompatibilityMixin, ABC):
|
|
66
|
+
"""Compatibility model for hosts adopting :class:`AgentRuntime` incrementally.
|
|
67
|
+
|
|
68
|
+
It preserves the established Pydantic fields and one-step behavior. New agents
|
|
69
|
+
should use :class:`inquirycraft.runtime.AgentRuntime` directly.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name: str | None = Field(None, description="Unique name of the agent")
|
|
73
|
+
description: str | None = Field(None, description="Optional agent description")
|
|
74
|
+
systemPrompt: str | list[dict] | None = Field(
|
|
75
|
+
None,
|
|
76
|
+
description="System-level instruction prompt",
|
|
77
|
+
)
|
|
78
|
+
llm: Any | None = Field(None, description="Language model instance")
|
|
79
|
+
memory: Memory = Field(default_factory=Memory, description="Agent's memory store")
|
|
80
|
+
state: AgentState = Field(default=AgentState.IDLE, description="Current agent state")
|
|
81
|
+
availableTools: ToolCollection | None = Field(None, description="List of available tools")
|
|
82
|
+
toolChoices: TOOL_CHOICE_TYPE | None = Field(
|
|
83
|
+
ToolChoice.AUTO,
|
|
84
|
+
description="Tool choice strategy",
|
|
85
|
+
)
|
|
86
|
+
toolCalls: list[ToolCall] | None = Field(None, description="List of tool calls")
|
|
87
|
+
ToolConfig: dict | None = Field(None, description="Other tool configuration")
|
|
88
|
+
exec_mode: str | None = Field(None, description="execution mode:single turn or multiturn")
|
|
89
|
+
|
|
90
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
|
91
|
+
|
|
92
|
+
@model_validator(mode="after")
|
|
93
|
+
def initializeAgent(self) -> "BaseAgent":
|
|
94
|
+
if self.llm is None:
|
|
95
|
+
raise ValueError("llm is none!")
|
|
96
|
+
if not isinstance(self.memory, Memory):
|
|
97
|
+
self.memory = Memory()
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
async def run(self, request: str | list[dict] | None = None) -> str:
|
|
101
|
+
if self.state != AgentState.IDLE:
|
|
102
|
+
raise RuntimeError(f"Cannot run agent from state: {self.state}")
|
|
103
|
+
if request:
|
|
104
|
+
self.updateMemory(Role.USER, request)
|
|
105
|
+
result = await self.step()
|
|
106
|
+
if self.llm:
|
|
107
|
+
self.llm.chunk_queue = asyncio.Queue()
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
async def step(self) -> str:
|
|
111
|
+
response = await self.llm.ask(
|
|
112
|
+
messages=self.memory.messages,
|
|
113
|
+
system_msgs=[Message.system_message(self.systemPrompt)],
|
|
114
|
+
)
|
|
115
|
+
self.updateMemory(Role.ASSISTANT, response)
|
|
116
|
+
return response
|
|
117
|
+
|
|
118
|
+
def updateMemory(
|
|
119
|
+
self,
|
|
120
|
+
role: ROLE_TYPE,
|
|
121
|
+
content: str | Message,
|
|
122
|
+
extra_info: dict[str, Any] | None = None,
|
|
123
|
+
**kwargs: Any,
|
|
124
|
+
) -> None:
|
|
125
|
+
if isinstance(content, Message):
|
|
126
|
+
message = content
|
|
127
|
+
else:
|
|
128
|
+
factories = {
|
|
129
|
+
Role.USER: Message.user_message,
|
|
130
|
+
Role.SYSTEM: Message.system_message,
|
|
131
|
+
Role.ASSISTANT: Message.assistant_message,
|
|
132
|
+
Role.TOOL: lambda value, **kw: Message.tool_message(value, **kw),
|
|
133
|
+
}
|
|
134
|
+
if role not in factories:
|
|
135
|
+
raise ValueError(f"Unsupported message role: {role}")
|
|
136
|
+
factory = factories[role]
|
|
137
|
+
message = factory(content, **kwargs) if role == Role.TOOL else factory(content)
|
|
138
|
+
if extra_info:
|
|
139
|
+
self.memory.add_message(message, extra_info=extra_info)
|
|
140
|
+
else:
|
|
141
|
+
self.memory.add_message(message)
|
|
142
|
+
|
|
143
|
+
async def executeTool(self, command: ToolCall) -> str:
|
|
144
|
+
if not command or not command.function or not command.function.name:
|
|
145
|
+
return "Error: Invalid command format"
|
|
146
|
+
name = command.function.name
|
|
147
|
+
if self.availableTools is None or name not in self.availableTools.tool_map:
|
|
148
|
+
return f"Error: Unknown tool '{name}'"
|
|
149
|
+
try:
|
|
150
|
+
arguments = json.loads(command.function.arguments or "{}")
|
|
151
|
+
arguments["config"] = self.ToolConfig
|
|
152
|
+
logger.info("Activating tool: %r", name)
|
|
153
|
+
result = await self.availableTools.execute(name=name, tool_input=arguments)
|
|
154
|
+
observation = (
|
|
155
|
+
f"Observed output of cmd `{name}` executed:\n{result}"
|
|
156
|
+
if result
|
|
157
|
+
else f"Cmd `{name}` completed with no output"
|
|
158
|
+
)
|
|
159
|
+
await self._handleSpecialTool(name=name, result=result)
|
|
160
|
+
return observation
|
|
161
|
+
except json.JSONDecodeError:
|
|
162
|
+
logger.error("Invalid JSON arguments for %r: %s", name, command.function.arguments)
|
|
163
|
+
return f"Error: Error parsing arguments for {name}: Invalid JSON format"
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
error = f"Tool '{name}' encountered a problem: {exc}"
|
|
166
|
+
logger.error(error)
|
|
167
|
+
return f"Error: {error}"
|
|
168
|
+
|
|
169
|
+
async def _handleSpecialTool(self, name: str, result: Any, **kwargs: Any) -> None:
|
|
170
|
+
if self._isSpecialTool(name) and self._shouldFinishExecution(
|
|
171
|
+
name=name,
|
|
172
|
+
result=result,
|
|
173
|
+
**kwargs,
|
|
174
|
+
):
|
|
175
|
+
self.state = AgentState.FINISHED
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _shouldFinishExecution(**kwargs: Any) -> bool:
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def _isSpecialTool(self, name: str) -> bool:
|
|
182
|
+
names = getattr(self, "special_tool_names", ())
|
|
183
|
+
return name.lower() in [value.lower() for value in names]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def messages(self) -> list[Message]:
|
|
187
|
+
return self.memory.messages
|
|
188
|
+
|
|
189
|
+
@messages.setter
|
|
190
|
+
def messages(self, value: list[Message]) -> None:
|
|
191
|
+
self.memory.messages = value
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
__all__ = ["BaseAgent", "MemoryRetrievalCompatibilityMixin"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .app import create_cli, main
|
|
2
|
+
from .contracts import CliExtension, CommandContext, CommandRegistry, RuntimeFactory
|
|
3
|
+
from .factory import DefaultRuntimeFactory
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CliExtension",
|
|
7
|
+
"CommandContext",
|
|
8
|
+
"CommandRegistry",
|
|
9
|
+
"DefaultRuntimeFactory",
|
|
10
|
+
"RuntimeFactory",
|
|
11
|
+
"create_cli",
|
|
12
|
+
"main",
|
|
13
|
+
]
|
inquirycraft/cli/app.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from inquirycraft.events import JsonlEventSink, NullEventSink, load_events
|
|
13
|
+
from inquirycraft.runtime import RuntimeOptions
|
|
14
|
+
from inquirycraft.tools import default_tools
|
|
15
|
+
|
|
16
|
+
from .contracts import CliExtension, CommandRegistry, RuntimeFactory
|
|
17
|
+
from .factory import DefaultRuntimeFactory
|
|
18
|
+
|
|
19
|
+
RuntimeOptionsDecorator = Callable[[Callable[..., Any]], Callable[..., Any]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _runtime_options(
|
|
23
|
+
*,
|
|
24
|
+
model_envvar: str | Sequence[str],
|
|
25
|
+
base_url_envvar: str | Sequence[str],
|
|
26
|
+
api_key_env_default: str,
|
|
27
|
+
) -> RuntimeOptionsDecorator:
|
|
28
|
+
def decorate(function):
|
|
29
|
+
options = [
|
|
30
|
+
click.option("--model", envvar=model_envvar, required=True, help="Model name."),
|
|
31
|
+
click.option("--base-url", envvar=base_url_envvar, default=None),
|
|
32
|
+
click.option(
|
|
33
|
+
"--api-key-env",
|
|
34
|
+
default=api_key_env_default,
|
|
35
|
+
show_default=True,
|
|
36
|
+
help="Environment variable containing the API key.",
|
|
37
|
+
),
|
|
38
|
+
click.option(
|
|
39
|
+
"--workspace",
|
|
40
|
+
type=click.Path(path_type=Path, file_okay=False),
|
|
41
|
+
default=Path("."),
|
|
42
|
+
show_default=True,
|
|
43
|
+
),
|
|
44
|
+
click.option("--system-prompt", default="You are a helpful agent.", show_default=True),
|
|
45
|
+
click.option("--max-turns", type=click.IntRange(1), default=20, show_default=True),
|
|
46
|
+
click.option("--max-tokens", type=click.IntRange(1), default=None),
|
|
47
|
+
click.option(
|
|
48
|
+
"--timeout",
|
|
49
|
+
type=click.FloatRange(min=0.1),
|
|
50
|
+
default=300.0,
|
|
51
|
+
show_default=True,
|
|
52
|
+
),
|
|
53
|
+
click.option(
|
|
54
|
+
"--event-log",
|
|
55
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
56
|
+
default=None,
|
|
57
|
+
),
|
|
58
|
+
click.option(
|
|
59
|
+
"--session-log",
|
|
60
|
+
type=click.Path(path_type=Path, dir_okay=False),
|
|
61
|
+
default=None,
|
|
62
|
+
help="Append conversation records to JSONL; existing records are resumed.",
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
for option in reversed(options):
|
|
66
|
+
function = option(function)
|
|
67
|
+
return function
|
|
68
|
+
|
|
69
|
+
return decorate
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build_runtime(
|
|
73
|
+
factory: RuntimeFactory,
|
|
74
|
+
*,
|
|
75
|
+
model: str,
|
|
76
|
+
base_url: str | None,
|
|
77
|
+
api_key_env: str,
|
|
78
|
+
workspace: Path,
|
|
79
|
+
system_prompt: str,
|
|
80
|
+
max_turns: int,
|
|
81
|
+
max_tokens: int | None,
|
|
82
|
+
timeout: float,
|
|
83
|
+
event_log: Path | None,
|
|
84
|
+
session_log: Path | None,
|
|
85
|
+
):
|
|
86
|
+
api_key = os.environ.get(api_key_env, "").strip()
|
|
87
|
+
if not api_key:
|
|
88
|
+
raise click.ClickException(f"API key environment variable {api_key_env!r} is empty")
|
|
89
|
+
sink = JsonlEventSink(event_log) if event_log else NullEventSink()
|
|
90
|
+
options = RuntimeOptions(
|
|
91
|
+
model=model,
|
|
92
|
+
workspace=workspace,
|
|
93
|
+
system_prompt=system_prompt,
|
|
94
|
+
max_turns=max_turns,
|
|
95
|
+
max_tokens=max_tokens,
|
|
96
|
+
timeout=timeout,
|
|
97
|
+
session_log=session_log,
|
|
98
|
+
)
|
|
99
|
+
return factory.create_runtime(
|
|
100
|
+
options,
|
|
101
|
+
api_key=api_key,
|
|
102
|
+
base_url=base_url,
|
|
103
|
+
event_sink=sink,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _register_run_command(
|
|
108
|
+
app: click.Group,
|
|
109
|
+
factory: RuntimeFactory,
|
|
110
|
+
options: RuntimeOptionsDecorator,
|
|
111
|
+
) -> None:
|
|
112
|
+
@app.command("run")
|
|
113
|
+
@click.argument("task")
|
|
114
|
+
@options
|
|
115
|
+
def run_command(task: str, **kwargs) -> None:
|
|
116
|
+
"""Run one generic agent task."""
|
|
117
|
+
runtime = _build_runtime(factory, **kwargs)
|
|
118
|
+
|
|
119
|
+
async def execute() -> None:
|
|
120
|
+
try:
|
|
121
|
+
result = await runtime.run(task)
|
|
122
|
+
click.echo(result)
|
|
123
|
+
finally:
|
|
124
|
+
await runtime.aclose()
|
|
125
|
+
|
|
126
|
+
asyncio.run(execute())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _register_repl_command(
|
|
130
|
+
app: click.Group,
|
|
131
|
+
factory: RuntimeFactory,
|
|
132
|
+
options: RuntimeOptionsDecorator,
|
|
133
|
+
) -> None:
|
|
134
|
+
@app.command("repl")
|
|
135
|
+
@options
|
|
136
|
+
def repl_command(**kwargs) -> None:
|
|
137
|
+
"""Start a generic persistent agent session."""
|
|
138
|
+
runtime = _build_runtime(factory, **kwargs)
|
|
139
|
+
|
|
140
|
+
async def execute() -> None:
|
|
141
|
+
try:
|
|
142
|
+
while True:
|
|
143
|
+
line = (await asyncio.to_thread(input, "inquirycraft> ")).strip()
|
|
144
|
+
if line.lower() in {"exit", "quit", "q"}:
|
|
145
|
+
break
|
|
146
|
+
if line:
|
|
147
|
+
click.echo(await runtime.run(line))
|
|
148
|
+
except (EOFError, KeyboardInterrupt):
|
|
149
|
+
pass
|
|
150
|
+
finally:
|
|
151
|
+
await runtime.aclose()
|
|
152
|
+
|
|
153
|
+
asyncio.run(execute())
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _register_tool_commands(app: click.Group) -> None:
|
|
157
|
+
@app.group("tools")
|
|
158
|
+
def tools_group() -> None:
|
|
159
|
+
"""Inspect built-in tools without initializing an LLM."""
|
|
160
|
+
|
|
161
|
+
@tools_group.command("list")
|
|
162
|
+
@click.option("--json-output", "json_output", is_flag=True)
|
|
163
|
+
def tools_list(json_output: bool) -> None:
|
|
164
|
+
tools = default_tools()
|
|
165
|
+
if json_output:
|
|
166
|
+
click.echo(json.dumps({"schema_version": "1.0", "tools": tools.names()}, indent=2))
|
|
167
|
+
else:
|
|
168
|
+
for tool_name in tools.names():
|
|
169
|
+
click.echo(tool_name)
|
|
170
|
+
|
|
171
|
+
@tools_group.command("schema")
|
|
172
|
+
@click.argument("name", required=False)
|
|
173
|
+
def tools_schema(name: str | None) -> None:
|
|
174
|
+
tools = default_tools()
|
|
175
|
+
schemas = tools.schemas()
|
|
176
|
+
if name:
|
|
177
|
+
schemas = tuple(row for row in schemas if row["function"]["name"] == name)
|
|
178
|
+
if not schemas:
|
|
179
|
+
raise click.ClickException(f"Unknown tool: {name}")
|
|
180
|
+
click.echo(json.dumps({"schema_version": "1.0", "tools": schemas}, indent=2))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _register_event_commands(app: click.Group) -> None:
|
|
184
|
+
@app.group("events")
|
|
185
|
+
def events_group() -> None:
|
|
186
|
+
"""Inspect InquiryCraft runtime event JSONL."""
|
|
187
|
+
|
|
188
|
+
@events_group.command("show")
|
|
189
|
+
@click.argument("path", type=click.Path(path_type=Path, exists=True, dir_okay=False))
|
|
190
|
+
@click.option("--type", "event_type", default=None)
|
|
191
|
+
@click.option("--limit", type=click.IntRange(1), default=None)
|
|
192
|
+
def events_show(path: Path, event_type: str | None, limit: int | None) -> None:
|
|
193
|
+
events = load_events(path, event_type=event_type)
|
|
194
|
+
if limit is not None:
|
|
195
|
+
events = events[-limit:]
|
|
196
|
+
for event in events:
|
|
197
|
+
click.echo(json.dumps(event.to_dict(), ensure_ascii=False, sort_keys=True))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def create_cli(
|
|
201
|
+
*,
|
|
202
|
+
name: str = "inquirycraft",
|
|
203
|
+
runtime_factory: RuntimeFactory | None = None,
|
|
204
|
+
extensions: Iterable[CliExtension] = (),
|
|
205
|
+
model_envvar: str | Sequence[str] = "INQUIRYCRAFT_MODEL",
|
|
206
|
+
base_url_envvar: str | Sequence[str] = "INQUIRYCRAFT_BASE_URL",
|
|
207
|
+
api_key_env_default: str = "INQUIRYCRAFT_API_KEY",
|
|
208
|
+
) -> click.Group:
|
|
209
|
+
factory = runtime_factory or DefaultRuntimeFactory()
|
|
210
|
+
|
|
211
|
+
@click.group(name=name)
|
|
212
|
+
def app() -> None:
|
|
213
|
+
"""Run and inspect lightweight InquiryCraft agents."""
|
|
214
|
+
|
|
215
|
+
options = _runtime_options(
|
|
216
|
+
model_envvar=model_envvar,
|
|
217
|
+
base_url_envvar=base_url_envvar,
|
|
218
|
+
api_key_env_default=api_key_env_default,
|
|
219
|
+
)
|
|
220
|
+
_register_run_command(app, factory, options)
|
|
221
|
+
_register_repl_command(app, factory, options)
|
|
222
|
+
_register_tool_commands(app)
|
|
223
|
+
_register_event_commands(app)
|
|
224
|
+
|
|
225
|
+
registry = CommandRegistry(app)
|
|
226
|
+
for extension in extensions:
|
|
227
|
+
extension.register(registry)
|
|
228
|
+
return app
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
main = create_cli()
|