Types and Tools¶
Register Types with @kedi.type¶
@kedi.type makes Python classes usable in Kedi output annotations and Python
expressions:
Bare classes are converted to standard dataclasses. The decorated name is rebound to the resulting class.
Pydantic Models¶
Existing Pydantic models are retained:
from pydantic import BaseModel, Field
@kedi.type
class Review(BaseModel):
score: float = Field(ge=0.0, le=1.0)
summary: str
@kedi.query
def review(text: str) -> Review:
"""kedi
>> Review of <text>: [result: Review].
= `result`
"""
...
The selected adapter receives the Pydantic schema. Validation is performed by the adapter/model integration when producing the typed output.
Pydantic Dataclasses¶
Pydantic dataclasses are recognized without reconversion:
from pydantic.dataclasses import dataclass
@kedi.type
@dataclass
class Entity:
name: str
confidence: float
Place @kedi.type above @dataclass so it receives the finished Pydantic
dataclass.
Standard Dataclasses¶
Standard dataclasses are also retained:
from dataclasses import dataclass
@kedi.type
@dataclass
class Coordinate:
latitude: float
longitude: float
Whether an adapter can produce a particular dataclass schema depends on that adapter's structured-output support.
Bare Class Conversion¶
For a simple record, no explicit dataclass decorator is required:
Kedi applies dataclasses.dataclass. Methods and supported dataclass defaults
remain available. Classes that require custom metaclass behavior should be
defined explicitly rather than relying on conversion.
Automatic Type Injection¶
inject=True is the default. The type is injected only for query/bind
callables defined in the same Python module:
Disable implicit injection:
@kedi.type(inject=False)
class InternalResult:
value: str
@kedi.query(env={"InternalResult": InternalResult})
def extract(text: str) -> InternalResult:
"""kedi
>> Structured representation of <text>: [result: InternalResult].
= `result`
"""
...
Configured and local env values override auto-injected type names. This can
be useful for dynamic schemas but should be deliberate.
Register Tools with @kedi.tool¶
The tool decorator preserves a callable while attaching adapter metadata:
@kedi.tool
def lookup_order(order_id: str) -> dict[str, object]:
"""Return public status information for one order."""
return {"id": order_id, "status": "queued"}
Register it through configure, context, query, or bind, then opt in
inside Kedi:
@kedi.query(tools=[lookup_order])
def answer(question: str) -> str:
"""kedi
> use: lookup_order
>> Use the order lookup when needed. Return [answer: str] for <question>.
= `answer`
"""
...
Decoration alone does not make the tool globally available.
Names, Descriptions, and Retries¶
Override metadata and retry transient callable errors:
@kedi.tool(
name="search_docs",
description="Search approved project documentation.",
retries=2,
risk="read_only",
)
def search_index(query: str) -> list[str]:
return index.search(query)
The registered name defaults to __name__; description defaults to the
docstring. retries=2 means at most three total attempts. Retries catch normal
Exception failures for both sync and async callables; they do not retry
BaseException subclasses. Negative retry counts are invalid.
Tool Risk Metadata¶
Every tool is classified as:
read_only;mutating;sensitive.
Custom tools default to mutating. Mark a tool read_only only if it cannot
change external or local state and cannot expose sensitive data:
Risk participates in approval. Read-only calls are automatically allowed; mutating and sensitive calls require an allow policy or dynamic decision.
Tool Argument Shadowing¶
The runtime environment cannot safely contain a function argument and a tool under the same registered name:
@kedi.tool(name="search")
def search_docs(query: str) -> list[str]:
return []
@kedi.query(tools=[search_docs])
def answer(search: str) -> str:
"""kedi
= <search>
"""
...
Calling answer(...) raises KediExecutionError. Rename either the parameter
or the tool. Kedi rejects the collision rather than allowing input to shadow a
callable capability.