Azimkin ver
This commit is contained in:
2
src/micro_agents/__init__.py
Normal file
2
src/micro_agents/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
def main() -> None:
|
||||
print("Hello from micro-agents!")
|
||||
28
src/micro_agents/aggregator.py
Normal file
28
src/micro_agents/aggregator.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Iterable
|
||||
|
||||
from micro_agents.conversation import Message
|
||||
from micro_agents.llm import LLM
|
||||
|
||||
|
||||
class EventSubscriber(ABC):
|
||||
@abstractmethod
|
||||
async def on_message(self, messages: Iterable[Message]):
|
||||
pass
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self, event_subscriber: EventSubscriber, session_name: str | None = None, messages: list[Message] | None = None):
|
||||
|
||||
|
||||
|
||||
|
||||
class Aggregator:
|
||||
def __init__(self, llm: LLM):
|
||||
self._llm = llm
|
||||
|
||||
def create_session(self):
|
||||
...
|
||||
|
||||
async def start_loop(self):
|
||||
...
|
||||
75
src/micro_agents/conversation.py
Normal file
75
src/micro_agents/conversation.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import json
|
||||
from abc import ABC
|
||||
from enum import Enum
|
||||
from typing import cast
|
||||
|
||||
|
||||
class Side(Enum):
|
||||
REASONING = "reasoning"
|
||||
USER = "user"
|
||||
LLM = "llm"
|
||||
TOOL_CALL = "tool_call"
|
||||
TOOL_RESPONSE = "tool_response"
|
||||
|
||||
|
||||
class Message(ABC):
|
||||
def __init__(self, side: Side, content: dict[str, object]):
|
||||
self.side = side
|
||||
self.content = content
|
||||
|
||||
|
||||
class ReasoningMessage(Message):
|
||||
def __init__(self, text: str):
|
||||
super().__init__(Side.REASONING, {'text': text})
|
||||
|
||||
def text(self) -> str:
|
||||
return self.content["text"].__str__()
|
||||
|
||||
|
||||
class UserMessage(Message):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(Side.USER, {'message': message})
|
||||
|
||||
def message(self) -> str:
|
||||
return self.content["message"].__str__()
|
||||
|
||||
|
||||
class LlmMessage(Message):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(Side.LLM, {'message': message})
|
||||
|
||||
def message(self) -> str:
|
||||
return self.content["message"].__str__()
|
||||
|
||||
|
||||
class ToolCallMessage(Message):
|
||||
def __init__(self, unique_id: str, tool_name: str, arguments: dict[str, object]):
|
||||
super().__init__(Side.TOOL_CALL, {"unique_id": unique_id, 'tool_name': tool_name, 'arguments': arguments})
|
||||
|
||||
def unique_id(self) -> str:
|
||||
return self.content["unique_id"].__str__()
|
||||
|
||||
def arguments(self) -> dict[str, object]:
|
||||
return cast(dict[str, object], self.content["arguments"])
|
||||
|
||||
def tool_name(self):
|
||||
return self.content["tool_call"].__str__()
|
||||
|
||||
|
||||
class ToolCallResponseMessage(Message):
|
||||
def __init__(self, unique_id: str, text: str):
|
||||
super().__init__(Side.TOOL_RESPONSE, {'unique_id': unique_id, 'text': text})
|
||||
|
||||
def text(self) -> str:
|
||||
return self.content["text"].__str__()
|
||||
|
||||
def unique_id(self) -> str:
|
||||
return self.content["unique_id"].__str__()
|
||||
|
||||
|
||||
class Conversation:
|
||||
def __init__(self, messages: list[Message]):
|
||||
self.messages = messages
|
||||
|
||||
def add_message(self, message: Message):
|
||||
self.messages.append(message)
|
||||
452
src/micro_agents/llm.py
Normal file
452
src/micro_agents/llm.py
Normal file
@@ -0,0 +1,452 @@
|
||||
from abc import ABC
|
||||
from typing import Iterable, Optional, Literal, cast, Any, Union, get_args, get_origin, get_type_hints, Annotated
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import inspect
|
||||
import re
|
||||
import types
|
||||
import collections.abc
|
||||
from uuid import uuid4
|
||||
|
||||
import orjson
|
||||
from openai import AsyncOpenAI
|
||||
from openai.types.responses import ResponseInputParam, ToolParam, ResponseInputItemParam, EasyInputMessageParam, \
|
||||
ResponseFunctionToolCallParam, ResponseCustomToolCallOutputParam, FunctionToolParam
|
||||
|
||||
from micro_agents.conversation import Conversation, UserMessage, LlmMessage, ToolCallMessage, ToolCallResponseMessage, \
|
||||
ReasoningMessage
|
||||
import micro_agents.tools as tools_package
|
||||
|
||||
|
||||
def _safe_issubclass(tp: Any, classes: Any) -> bool:
|
||||
try:
|
||||
return isinstance(tp, type) and issubclass(tp, classes)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class LLM(ABC):
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
|
||||
class OpenAIBasedLLM(LLM):
|
||||
def __init__(self, api_url: str, api_key: str, model: str):
|
||||
super().__init__()
|
||||
self._ai = AsyncOpenAI(api_key=api_key, base_url=api_url, )
|
||||
self._model = model
|
||||
|
||||
async def ask(self, conversation: Conversation, tools: list[tools_package.Tool]):
|
||||
response = await self._ai.responses.create(
|
||||
model=self._model,
|
||||
input=self._serialize_conversation(conversation),
|
||||
tools=self._serialize_tools(tools)
|
||||
)
|
||||
out_messages = []
|
||||
for item in response.output:
|
||||
match item.type:
|
||||
case "reasoning":
|
||||
reasoning_parts = []
|
||||
for c in item.content:
|
||||
reasoning_parts.append(c.text)
|
||||
out_messages.append(ReasoningMessage(" ".join(reasoning_parts)))
|
||||
case 'message':
|
||||
out_messages.append(LlmMessage(response.output_text))
|
||||
case "function_call":
|
||||
out_messages.append(ToolCallMessage(item.call_id, item.name, orjson.loads(item.arguments)))
|
||||
|
||||
return out_messages
|
||||
|
||||
def _serialize_conversation(self, conversation: Conversation) -> ResponseInputParam:
|
||||
out = []
|
||||
for m in conversation.messages:
|
||||
if isinstance(m, UserMessage):
|
||||
out.append(self._serialize_user_message(m))
|
||||
elif isinstance(m, LlmMessage):
|
||||
out.append(self._serialize_llm_message(m))
|
||||
elif isinstance(m, ToolCallMessage):
|
||||
out.append(self._serialize_tool_call_message(m))
|
||||
elif isinstance(m, ToolCallResponseMessage):
|
||||
out.append(self._serialize_tool_call_response_message(m))
|
||||
else:
|
||||
raise Exception(f"Unknown type of message {type(m).__name__}")
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _serialize_user_message(message: UserMessage) -> EasyInputMessageParam:
|
||||
return {
|
||||
'role': 'user',
|
||||
'content': message.message()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_llm_message(message: LlmMessage) -> EasyInputMessageParam:
|
||||
return {
|
||||
'role': 'assistant',
|
||||
'content': message.message(),
|
||||
'phase': cast(Optional[Literal["commentary", "final_answer"]], message.content["phase"])
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_tool_call_message(message: ToolCallMessage) -> ResponseFunctionToolCallParam:
|
||||
return {
|
||||
'type': 'function_call',
|
||||
'call_id': message.unique_id(),
|
||||
'name': message.tool_name(),
|
||||
'arguments': orjson.dumps(message.arguments()).decode('utf-8'),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _serialize_tool_call_response_message(message: ToolCallResponseMessage) -> ResponseCustomToolCallOutputParam:
|
||||
return {
|
||||
'type': 'custom_tool_call_output',
|
||||
'call_id': message.unique_id(),
|
||||
'output': message.text()
|
||||
}
|
||||
|
||||
def _serialize_tools(self, tools: list[tools_package.Tool]) -> Iterable[FunctionToolParam]:
|
||||
return (self._serialize_tool(tool) for tool in tools)
|
||||
|
||||
def _serialize_tool(self, tool: tools_package.Tool) -> FunctionToolParam:
|
||||
fn = tool.executor
|
||||
name = self._infer_tool_name(tool, fn)
|
||||
description = self._infer_tool_description(tool, fn)
|
||||
# Описания параметров: блок Params в description тула > param_descriptions > docstring функции.
|
||||
extra: dict[str, str] = {}
|
||||
tool_desc_raw = getattr(tool, "description", None)
|
||||
if isinstance(tool_desc_raw, str) and tool_desc_raw.strip():
|
||||
extra.update(self._parse_docstring(tool_desc_raw)[1])
|
||||
override = getattr(tool, "param_descriptions", None)
|
||||
if isinstance(override, dict):
|
||||
extra.update({str(k): str(v) for k, v in override.items() if isinstance(v, str) and v.strip()})
|
||||
parameters: dict[str, object] = self._infer_parameters_schema(fn, extra)
|
||||
strict = tool.strict
|
||||
return {
|
||||
"type": "function",
|
||||
"name": name,
|
||||
"description": description,
|
||||
"parameters": parameters,
|
||||
"strict": bool(strict),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _infer_tool_name(tool: tools_package.Tool, fn: Any) -> str:
|
||||
candidate = getattr(tool, "name", None) or getattr(fn, "__name__", None)
|
||||
if not candidate or candidate == "<lambda>":
|
||||
candidate = getattr(fn, "__qualname__", None) or fn.__class__.__name__
|
||||
candidate = str(candidate)
|
||||
sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", candidate)
|
||||
sanitized = sanitized.strip("_") or "tool"
|
||||
return sanitized[:64]
|
||||
|
||||
@staticmethod
|
||||
def _infer_tool_description(tool: tools_package.Tool, fn: Any) -> Optional[str]:
|
||||
desc = getattr(tool, "description", None)
|
||||
if isinstance(desc, str) and desc.strip():
|
||||
# Чистим от блока с описаниями параметров, если он там есть.
|
||||
return OpenAIBasedLLM._parse_docstring(desc)[0] or desc.strip()
|
||||
doc = inspect.getdoc(fn)
|
||||
if doc:
|
||||
return OpenAIBasedLLM._parse_docstring(doc)[0]
|
||||
return None
|
||||
|
||||
# Заголовки блока с описаниями параметров (включая русские).
|
||||
_PARAM_SECTION_NAMES = ("args", "arguments", "params", "parameters", "параметры", "аргументы")
|
||||
_END_SECTION_NAMES = ("returns", "return", "yields", "yield", "raises", "raise",
|
||||
"examples", "example", "notes", "note", "see also")
|
||||
|
||||
@staticmethod
|
||||
def _parse_docstring(doc: str) -> tuple[Optional[str], dict[str, str]]:
|
||||
"""Разобрать docstring: вернуть (чистое описание, {параметр: описание}).
|
||||
|
||||
Понимает блоки вида::
|
||||
|
||||
'''Краткое описание.
|
||||
|
||||
Params:
|
||||
city: город, например "Berlin".
|
||||
units: единицы измерения.
|
||||
'''
|
||||
|
||||
а также стили Google (``Args:``), Sphinx (``:param name:``)
|
||||
и NumPy (``Parameters\\n----------``).
|
||||
"""
|
||||
descriptions: dict[str, str] = {}
|
||||
text = doc.strip()
|
||||
# 1. Sphinx-style: :param name: desc / @param name: desc
|
||||
for m in re.finditer(
|
||||
r"^[ \t]*(?::(?:param|parameter|arg|argument)\s+(\w+)\s*:|@param\s+(\w+)\s*:?)[ \t]*(.+?)[ \t]*$",
|
||||
text, re.MULTILINE,
|
||||
):
|
||||
descriptions.setdefault(m.group(1) or m.group(2), m.group(3).strip())
|
||||
# 2. Google-style / свой блок: "Args:" / "Params:" / "Параметры:" ...
|
||||
header_pat = r"(?im)^[ \t]*(" + "|".join(OpenAIBasedLLM._PARAM_SECTION_NAMES) + r")[ \t]*:[ \t]*$"
|
||||
m = re.search(header_pat, text)
|
||||
head = text
|
||||
params_body = ""
|
||||
if m:
|
||||
head = text[:m.start()]
|
||||
rest = text[m.end():]
|
||||
end = re.search(r"(?im)^[ \t]*\w[\w ]*[ \t]*:[ \t]*$", rest)
|
||||
params_body = rest[:end.start()] if end else rest
|
||||
for pname, pdesc in OpenAIBasedLLM._parse_google_param_block(params_body).items():
|
||||
descriptions.setdefault(pname, pdesc)
|
||||
else:
|
||||
# 3. NumPy-style: "Parameters\n----------"
|
||||
nm = re.search(r"(?m)^[ \t]*(Parameters|Параметры)[ \t]*\n[ \t]*-{3,}[ \t]*$", text)
|
||||
if nm:
|
||||
head = text[:nm.start()]
|
||||
rest = text[nm.end():]
|
||||
end = re.search(r"(?m)^[ \t]*\w[\w ]*[ \t]*\n[ \t]*-{3,}[ \t]*$", rest)
|
||||
params_body = rest[:end.start()] if end else rest
|
||||
end2 = re.search(r"(?im)^[ \t]*\w[\w ]*[ \t]*:[ \t]*$", params_body)
|
||||
if end2:
|
||||
params_body = params_body[:end2.start()]
|
||||
for pname, pdesc in OpenAIBasedLLM._parse_numpy_param_block(params_body).items():
|
||||
descriptions.setdefault(pname, pdesc)
|
||||
# Чистим описание тула: отрезаем хвостовые секции и sphinx-поля.
|
||||
head = re.split(r"(?im)^[ \t]*(returns?|yields?|raises?|examples?|notes?)[ \t]*:[ \t]*$", head)[0]
|
||||
head = re.sub(
|
||||
r"(?m)^[ \t]*(@param\b.*|:(?:param|parameter|arg|argument|type|return|returns|rtype|raises?)\b.*)$",
|
||||
"", head,
|
||||
)
|
||||
return head.strip() or None, descriptions
|
||||
|
||||
@staticmethod
|
||||
def _parse_google_param_block(body: str) -> dict[str, str]:
|
||||
"""Строки ``name: desc`` / ``name (type): desc`` / ``name - desc`` + продолжения."""
|
||||
out: dict[str, str] = {}
|
||||
current: Optional[str] = None
|
||||
pat = re.compile(r"^[ \t]*\*{0,2}(\w+)[ \t]*(?:\([^)]*\))?[ \t]*[:\-–—][ \t]*(.+?)[ \t]*$")
|
||||
for line in body.splitlines():
|
||||
m = pat.match(line)
|
||||
if m and m.group(1).lower() not in OpenAIBasedLLM._END_SECTION_NAMES:
|
||||
current = m.group(1)
|
||||
out[current] = m.group(2).strip()
|
||||
elif current is not None and line.strip() and line[:1] in (" ", "\t"):
|
||||
out[current] += " " + line.strip()
|
||||
elif line.strip():
|
||||
current = None
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _parse_numpy_param_block(body: str) -> dict[str, str]:
|
||||
"""Строки ``name : type`` + описание с отступом; ``name - desc`` в одну строку."""
|
||||
out: dict[str, str] = {}
|
||||
current: Optional[str] = None
|
||||
dash_pat = re.compile(r"^[ \t]*\*{0,2}(\w+)[ \t]*[–—-][ \t]*(.+?)[ \t]*$")
|
||||
entry_pat = re.compile(r"^[ \t]*\*{0,2}(\w+)[ \t]*(?::[^\n]*)?$")
|
||||
for line in body.splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
indented = line[:1] in (" ", "\t")
|
||||
dm = dash_pat.match(line)
|
||||
if dm:
|
||||
current = dm.group(1)
|
||||
out[current] = dm.group(2).strip()
|
||||
elif not indented:
|
||||
em = entry_pat.match(line)
|
||||
if em and em.group(1).lower() not in OpenAIBasedLLM._END_SECTION_NAMES:
|
||||
current = em.group(1)
|
||||
out.setdefault(current, "")
|
||||
else:
|
||||
current = None
|
||||
elif current is not None:
|
||||
piece = line.strip()
|
||||
out[current] = (out[current] + " " + piece).strip() if out[current] else piece
|
||||
return {k: v for k, v in out.items() if v}
|
||||
|
||||
@staticmethod
|
||||
def _infer_parameters_schema(fn: Any, param_descriptions: Optional[dict[str, str]] = None) -> dict[str, object]:
|
||||
try:
|
||||
sig = inspect.signature(fn)
|
||||
except (ValueError, TypeError):
|
||||
return {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
|
||||
try:
|
||||
hints = get_type_hints(fn, include_extras=True)
|
||||
except Exception:
|
||||
hints = dict(getattr(fn, "__annotations__", {}) or {})
|
||||
# isinstance get_type_hints may return non-dict in weird cases; guard
|
||||
if not isinstance(hints, dict):
|
||||
hints = {}
|
||||
# Описания параметров из блока в docstring функции (Annotated в приоритете — см. ниже).
|
||||
doc = inspect.getdoc(fn) or ""
|
||||
merged: dict[str, str] = OpenAIBasedLLM._parse_docstring(doc)[1] if doc else {}
|
||||
if param_descriptions:
|
||||
merged.update(param_descriptions)
|
||||
|
||||
params = list(sig.parameters.values())
|
||||
skip_first_self = False
|
||||
if params and params[0].name in ("self", "cls"):
|
||||
qualname = getattr(fn, "__qualname__", "")
|
||||
if "." in qualname or inspect.ismethod(fn) or inspect.isclass(getattr(fn, "__self__", None)):
|
||||
skip_first_self = True
|
||||
|
||||
properties: dict[str, object] = {}
|
||||
required: list[str] = []
|
||||
for i, param in enumerate(params):
|
||||
if i == 0 and skip_first_self:
|
||||
continue
|
||||
if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
|
||||
continue
|
||||
ann = hints.get(param.name, param.annotation)
|
||||
schema: dict[str, object] = dict(OpenAIBasedLLM._python_type_to_json_schema(ann))
|
||||
# description из docstring — только если её ещё нет (Annotated в приоритете).
|
||||
if "description" not in schema and param.name in merged:
|
||||
schema["description"] = merged[param.name]
|
||||
has_default = param.default is not inspect.Parameter.empty
|
||||
if has_default:
|
||||
default = param.default
|
||||
if default is None or isinstance(default, (str, int, float, bool)):
|
||||
schema["default"] = default
|
||||
else:
|
||||
if not OpenAIBasedLLM._is_optional_annotation(ann):
|
||||
required.append(param.name)
|
||||
properties[param.name] = schema
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_structured_class(ann: Any) -> bool:
|
||||
return (
|
||||
(dataclasses.is_dataclass(ann) and isinstance(ann, type))
|
||||
or (isinstance(ann, type) and _safe_issubclass(ann, enum.Enum))
|
||||
or (hasattr(ann, "model_json_schema") or hasattr(ann, "__required_keys__"))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_optional_annotation(ann: Any) -> bool:
|
||||
if ann is None or ann is type(None):
|
||||
return True
|
||||
if ann is inspect.Parameter.empty:
|
||||
return False
|
||||
origin = get_origin(ann)
|
||||
if origin in (Union, types.UnionType):
|
||||
return any(a in (None, type(None)) for a in get_args(ann))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _python_type_to_json_schema(ann: Any) -> dict[str, object]:
|
||||
from typing import Literal as TypingLiteral
|
||||
if ann is None or ann is type(None):
|
||||
return {"type": "null"}
|
||||
if ann is inspect.Parameter.empty or ann is Any or ann is object:
|
||||
return {}
|
||||
# Annotated[X, ...] -> unwrap, first str metadata becomes description
|
||||
if get_origin(ann) is Annotated:
|
||||
args = get_args(ann)
|
||||
inner = OpenAIBasedLLM._python_type_to_json_schema(args[0]) if args else {}
|
||||
for meta in args[1:]:
|
||||
if isinstance(meta, str) and "description" not in inner:
|
||||
inner = {**inner, "description": meta}
|
||||
return inner
|
||||
# Literal[...] -> enum
|
||||
if get_origin(ann) in (TypingLiteral, Literal):
|
||||
return {"enum": list(get_args(ann))}
|
||||
origin = get_origin(ann)
|
||||
# Union / X | Y
|
||||
if origin is Union or origin is types.UnionType:
|
||||
args = get_args(ann)
|
||||
non_none = [a for a in args if a not in (None, type(None))]
|
||||
if len(non_none) == 1 and len(non_none) != len(args):
|
||||
# Optional[X] -> unwrap to inner (omittable via `required`)
|
||||
return OpenAIBasedLLM._python_type_to_json_schema(non_none[0])
|
||||
return {"anyOf": [OpenAIBasedLLM._python_type_to_json_schema(a) for a in args]}
|
||||
# primitives
|
||||
if ann is str:
|
||||
return {"type": "string"}
|
||||
if ann is int:
|
||||
return {"type": "integer"}
|
||||
if ann is float:
|
||||
return {"type": "number"}
|
||||
if ann is bool:
|
||||
return {"type": "boolean"}
|
||||
if ann is bytes:
|
||||
return {"type": "string", "format": "byte"}
|
||||
# list / sequence
|
||||
if ann in (list,):
|
||||
return {"type": "array"}
|
||||
if origin is not None and (
|
||||
origin in (list, set, frozenset, tuple, collections.abc.Sequence, collections.abc.MutableSequence)
|
||||
or _safe_issubclass(origin, (list, set, frozenset, tuple))
|
||||
):
|
||||
args = get_args(ann)
|
||||
if origin in (set, frozenset) or (isinstance(ann, type) and _safe_issubclass(ann, (set, frozenset))):
|
||||
items = OpenAIBasedLLM._python_type_to_json_schema(args[0]) if args else {}
|
||||
out: dict[str, object] = {"type": "array", "uniqueItems": True}
|
||||
if items:
|
||||
out["items"] = items
|
||||
return out
|
||||
if origin is tuple or ann is tuple:
|
||||
if len(args) == 2 and args[1] is Ellipsis:
|
||||
return {"type": "array", "items": OpenAIBasedLLM._python_type_to_json_schema(args[0])}
|
||||
if args:
|
||||
return {"type": "array", "prefixItems": [OpenAIBasedLLM._python_type_to_json_schema(a) for a in args]}
|
||||
return {"type": "array"}
|
||||
# list / Sequence / Iterable ...
|
||||
if args:
|
||||
return {"type": "array", "items": OpenAIBasedLLM._python_type_to_json_schema(args[0])}
|
||||
return {"type": "array"}
|
||||
if isinstance(ann, type) and _safe_issubclass(ann, (list, set, frozenset, tuple)):
|
||||
return {"type": "array"}
|
||||
# dict / mapping
|
||||
if origin is not None and (
|
||||
origin in (dict, collections.abc.Mapping, collections.abc.MutableMapping)
|
||||
or _safe_issubclass(origin, dict)
|
||||
):
|
||||
args = get_args(ann)
|
||||
if len(args) == 2:
|
||||
return {"type": "object", "additionalProperties": OpenAIBasedLLM._python_type_to_json_schema(args[1])}
|
||||
return {"type": "object"}
|
||||
if ann in (dict,) or (isinstance(ann, type) and _safe_issubclass(ann, dict) and not OpenAIBasedLLM._is_structured_class(ann)):
|
||||
return {"type": "object"}
|
||||
# Enum -> enum of values
|
||||
if isinstance(ann, type) and _safe_issubclass(ann, enum.Enum):
|
||||
try:
|
||||
values = [e.value for e in ann]
|
||||
except Exception:
|
||||
return {}
|
||||
return {"enum": values}
|
||||
# Pydantic v2 / v1 without hard dependency
|
||||
if hasattr(ann, "model_json_schema") and callable(getattr(ann, "model_json_schema")):
|
||||
try:
|
||||
schema = ann.model_json_schema() # type: ignore[union-attr]
|
||||
if isinstance(schema, dict):
|
||||
return schema
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(ann, "schema") and callable(getattr(ann, "schema")):
|
||||
try:
|
||||
schema = ann.schema() # type: ignore[union-attr]
|
||||
if isinstance(schema, dict):
|
||||
return schema
|
||||
except Exception:
|
||||
pass
|
||||
# dataclass -> object with properties
|
||||
if dataclasses.is_dataclass(ann) and isinstance(ann, type):
|
||||
try:
|
||||
hints = get_type_hints(ann, include_extras=True)
|
||||
except Exception:
|
||||
hints = dict(getattr(ann, "__annotations__", {}) or {})
|
||||
props = {f.name: OpenAIBasedLLM._python_type_to_json_schema(hints.get(f.name, f.type)) for f in dataclasses.fields(ann)}
|
||||
req = [f.name for f in dataclasses.fields(ann) if f.default is dataclasses.MISSING and f.default_factory is dataclasses.MISSING]
|
||||
return {"type": "object", "properties": props, "required": req, "additionalProperties": False}
|
||||
# TypedDict -> object with properties
|
||||
if isinstance(ann, type) and _safe_issubclass(ann, dict) and hasattr(ann, "__annotations__"):
|
||||
try:
|
||||
hints = get_type_hints(ann, include_extras=True)
|
||||
except Exception:
|
||||
hints = dict(getattr(ann, "__annotations__", {}) or {})
|
||||
if hints:
|
||||
props = {k: OpenAIBasedLLM._python_type_to_json_schema(v) for k, v in hints.items()}
|
||||
req = list(getattr(ann, "__required_keys__", list(hints.keys())))
|
||||
return {"type": "object", "properties": props, "required": list(req), "additionalProperties": False}
|
||||
# unknown class -> generic object / any
|
||||
if isinstance(ann, type):
|
||||
return {}
|
||||
return {}
|
||||
6
src/micro_agents/tools.py
Normal file
6
src/micro_agents/tools.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
class Tool:
|
||||
def __init__(self, executor: Callable, strict: bool = True):
|
||||
self.executor = executor
|
||||
self.strict = strict
|
||||
Reference in New Issue
Block a user