|
| 1 | +import abc |
| 2 | +import collections |
| 3 | +import functools |
1 | 4 | import inspect |
2 | 5 | import types |
3 | 6 | import typing |
4 | | -from collections import ChainMap |
5 | 7 | from collections.abc import Callable, Mapping, MutableMapping |
6 | 8 | from dataclasses import dataclass |
7 | 9 | from typing import Annotated, Any |
8 | 10 |
|
| 11 | +from effectful.ops.semantics import handler |
9 | 12 | from effectful.ops.types import INSTANCE_OP_PREFIX, Annotation, Operation |
10 | 13 |
|
11 | 14 |
|
@@ -183,7 +186,7 @@ class Template[**P, T](Tool[P, T]): |
183 | 186 |
|
184 | 187 | """ |
185 | 188 |
|
186 | | - __context__: ChainMap[str, Any] |
| 189 | + __context__: collections.ChainMap[str, Any] |
187 | 190 |
|
188 | 191 | @property |
189 | 192 | def __prompt_template__(self) -> str: |
@@ -283,11 +286,72 @@ def define[**Q, V]( |
283 | 286 | frame = frame.f_back |
284 | 287 |
|
285 | 288 | contexts.append(globals_proxy) |
286 | | - context: ChainMap[str, Any] = ChainMap( |
| 289 | + context: collections.ChainMap[str, Any] = collections.ChainMap( |
287 | 290 | *typing.cast(list[MutableMapping[str, Any]], contexts) |
288 | 291 | ) |
289 | 292 |
|
290 | 293 | op = super().define(default, *args, **kwargs) |
291 | 294 | op.__context__ = context # type: ignore[attr-defined] |
292 | 295 |
|
293 | 296 | return typing.cast(Template[Q, V], op) |
| 297 | + |
| 298 | + |
| 299 | +class Agent(abc.ABC): |
| 300 | + """Mixin that gives each instance a persistent LLM message history. |
| 301 | +
|
| 302 | + Subclass and decorate methods with :func:`Template.define`. |
| 303 | + Each instance accumulates messages across calls so the LLM sees |
| 304 | + prior conversation context. |
| 305 | +
|
| 306 | + Agents compose freely with :func:`dataclasses.dataclass` and other |
| 307 | + base classes. Instance attributes are available in template |
| 308 | + docstrings via ``{self.attr}``. |
| 309 | +
|
| 310 | + Example:: |
| 311 | +
|
| 312 | + import dataclasses |
| 313 | + from effectful.handlers.llm import Agent, Template |
| 314 | + from effectful.handlers.llm.completions import LiteLLMProvider |
| 315 | + from effectful.ops.semantics import handler |
| 316 | + from effectful.ops.types import NotHandled |
| 317 | +
|
| 318 | + @dataclasses.dataclass |
| 319 | + class ChatBot(Agent): |
| 320 | + bot_name: str = dataclasses.field(default="ChatBot") |
| 321 | +
|
| 322 | + @Template.define |
| 323 | + def send(self, user_input: str) -> str: |
| 324 | + \"""Friendly bot named {self.bot_name}. User writes: {user_input}\""" |
| 325 | + raise NotHandled |
| 326 | +
|
| 327 | + provider = LiteLLMProvider() |
| 328 | + chatbot = ChatBot() |
| 329 | +
|
| 330 | + with handler(provider): |
| 331 | + chatbot.send("Hi! How are you? I am in France.") |
| 332 | + chatbot.send("Remind me again, where am I?") # sees prior context |
| 333 | +
|
| 334 | + """ |
| 335 | + |
| 336 | + __history__: collections.OrderedDict[str, Any] |
| 337 | + |
| 338 | + def __init_subclass__(cls, **kwargs): |
| 339 | + super().__init_subclass__(**kwargs) |
| 340 | + prop = functools.cached_property(lambda _: collections.OrderedDict()) |
| 341 | + prop.__set_name__(cls, "__history__") |
| 342 | + cls.__history__ = prop |
| 343 | + |
| 344 | + for name in list(cls.__dict__): |
| 345 | + attr = cls.__dict__[name] |
| 346 | + if not isinstance(attr, Template): |
| 347 | + continue |
| 348 | + _template = attr |
| 349 | + |
| 350 | + @functools.wraps(_template) |
| 351 | + def wrapper(self, *args, _t=_template, **kwargs): |
| 352 | + from effectful.handlers.llm.completions import get_message_sequence |
| 353 | + |
| 354 | + with handler({get_message_sequence: lambda: self.__history__}): |
| 355 | + return _t(self, *args, **kwargs) |
| 356 | + |
| 357 | + setattr(cls, name, wrapper) |
0 commit comments