From 77421cbb2a04e8b1c4cb2e180dc7d26d35a3fa59 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 10 Mar 2026 23:02:16 +0000 Subject: [PATCH 01/14] docs: expand module-level docstrings for API reference (#612) Replace one-liner and missing module docstrings across 53 files with substantive 2-4 sentence descriptions covering each module's purpose, key exports, and when to reach for it. Covers all three priority tiers from the issue: __init__.py landing pages, undocumented modules, and short stubs that merely restated the module name. --- cli/alora/commands.py | 9 +++++++++ cli/alora/intrinsic_uploader.py | 9 +++++++++ cli/alora/readme_generator.py | 10 ++++++++++ cli/alora/train.py | 9 +++++++++ cli/alora/upload.py | 8 ++++++++ cli/decompose/__init__.py | 9 +++++++++ cli/decompose/decompose.py | 9 +++++++++ cli/decompose/pipeline.py | 9 +++++++++ cli/decompose/utils.py | 9 +++++++++ cli/eval/__init__.py | 9 ++++++++- cli/eval/runner.py | 8 ++++++++ cli/m.py | 8 +++++++- mellea/backends/__init__.py | 10 +++++++++- mellea/backends/adapters/adapter.py | 10 +++++++++- mellea/backends/backend.py | 9 ++++++++- mellea/backends/cache.py | 9 ++++++++- mellea/backends/kv_block_helpers.py | 9 ++++++++- mellea/backends/model_ids.py | 9 ++++++++- mellea/backends/tools.py | 9 ++++++++- mellea/backends/utils.py | 9 ++++++++- mellea/core/__init__.py | 11 ++++++++++- mellea/core/backend.py | 9 ++++++++- mellea/core/base.py | 11 ++++++++++- mellea/core/formatter.py | 9 ++++++++- mellea/core/requirement.py | 10 +++++++++- mellea/core/sampling.py | 10 +++++++++- mellea/core/utils.py | 8 +++++++- mellea/formatters/__init__.py | 10 +++++++++- mellea/formatters/chat_formatter.py | 10 +++++++++- mellea/formatters/granite/base/optional.py | 8 +++++++- mellea/formatters/granite/base/types.py | 9 ++++++++- mellea/formatters/granite/intrinsics/json_util.py | 8 +++++++- mellea/formatters/template_formatter.py | 10 +++++++++- mellea/helpers/__init__.py | 11 ++++++++++- mellea/helpers/async_helpers.py | 10 +++++++++- mellea/helpers/server_type.py | 10 +++++++++- mellea/stdlib/__init__.py | 12 +++++++++++- mellea/stdlib/components/chat.py | 10 +++++++++- mellea/stdlib/components/docs/document.py | 8 +++++++- mellea/stdlib/components/docs/richdocument.py | 10 +++++++++- mellea/stdlib/components/instruction.py | 10 +++++++++- mellea/stdlib/components/intrinsic/intrinsic.py | 9 ++++++++- mellea/stdlib/components/mify.py | 11 ++++++++++- mellea/stdlib/components/mobject.py | 10 +++++++++- mellea/stdlib/components/react.py | 10 +++++++++- mellea/stdlib/components/simple.py | 8 +++++++- mellea/stdlib/context.py | 9 ++++++++- mellea/stdlib/frameworks/react.py | 9 ++++++++- mellea/stdlib/requirements/tool_reqs.py | 10 +++++++++- .../sampling/sampling_algos/budget_forcing_alg.py | 10 +++++++++- mellea/stdlib/session.py | 10 +++++++++- mellea/stdlib/tools/interpreter.py | 12 +++++++++++- 52 files changed, 450 insertions(+), 42 deletions(-) diff --git a/cli/alora/commands.py b/cli/alora/commands.py index 8a85067cb..c39d807a9 100644 --- a/cli/alora/commands.py +++ b/cli/alora/commands.py @@ -1,3 +1,12 @@ +"""Typer sub-application for the ``m alora`` command group. + +Provides three commands: ``train`` (fine-tune a base causal language model on a JSONL +dataset to produce a LoRA or aLoRA adapter), ``upload`` (push adapter weights to +Hugging Face Hub, optionally packaging the adapter as an intrinsic with an +``io.yaml`` configuration), and ``add-readme`` (use an LLM to auto-generate and +upload an ``INTRINSIC_README.md`` for the trained adapter). +""" + import json import os import tempfile diff --git a/cli/alora/intrinsic_uploader.py b/cli/alora/intrinsic_uploader.py index 299e7dcc5..43f5a3408 100644 --- a/cli/alora/intrinsic_uploader.py +++ b/cli/alora/intrinsic_uploader.py @@ -1,3 +1,12 @@ +"""Upload a trained adapter to Hugging Face Hub in the intrinsic directory layout. + +Creates or updates a private Hugging Face repository and uploads adapter weights +into a ``//`` sub-directory, together with +the required ``io.yaml`` configuration file. If an ``INTRINSIC_README.md`` exists in +the weight directory it is also uploaded as the repository's root ``README.md``. +Requires an authenticated Hugging Face token obtained via ``huggingface-cli login``. +""" + import os import shutil import tempfile diff --git a/cli/alora/readme_generator.py b/cli/alora/readme_generator.py index 0dd928ae6..7002bb96c 100644 --- a/cli/alora/readme_generator.py +++ b/cli/alora/readme_generator.py @@ -1,3 +1,13 @@ +"""LLM-assisted generator for adapter intrinsic README files. + +Uses a ``MelleaSession`` with rejection sampling to derive README template variables +from a JSONL training dataset — including a high-level description, the inferred +Python argument list, and Jinja2-renderable sample rows. Validates the generated +output with deterministic requirements (correct naming conventions, syntactically +valid argument lists) before rendering the final ``INTRINSIC_README.md`` via a +Jinja2 template. +""" + import ast import json import os diff --git a/cli/alora/train.py b/cli/alora/train.py index 408864cab..15bdc53a2 100644 --- a/cli/alora/train.py +++ b/cli/alora/train.py @@ -1,3 +1,12 @@ +"""Fine-tune a causal language model to produce a LoRA or aLoRA adapter. + +Loads a JSONL dataset of ``item``/``label`` pairs, applies an 80/20 train/validation +split, and trains using HuggingFace PEFT and TRL's ``SFTTrainer`` — saving the +checkpoint with the lowest validation loss. Supports CUDA, MPS (macOS, +PyTorch ≥ 2.8), and CPU device selection, and handles the +``alora_invocation_tokens`` configuration required for aLoRA training. +""" + import json import os import sys diff --git a/cli/alora/upload.py b/cli/alora/upload.py index a048778ee..09300e420 100644 --- a/cli/alora/upload.py +++ b/cli/alora/upload.py @@ -1,3 +1,11 @@ +"""Upload a trained LoRA or aLoRA adapter to Hugging Face Hub. + +Creates the target repository if it does not already exist and pushes the entire +adapter weights directory (output of ``save_pretrained``) to the repository root. +Requires an authenticated Hugging Face token set via the ``HF_TOKEN`` environment +variable or ``huggingface-cli login``. +""" + import os from huggingface_hub import HfApi, HfFolder, create_repo, upload_folder diff --git a/cli/decompose/__init__.py b/cli/decompose/__init__.py index b0d5d8402..76d37d5bf 100644 --- a/cli/decompose/__init__.py +++ b/cli/decompose/__init__.py @@ -1,3 +1,12 @@ +"""Typer sub-application for the ``m decompose`` command group. + +Exposes a single ``run`` command that takes a task prompt (from a file or +interactively), calls the LLM-based decomposition pipeline to break it into +structured subtasks with constraints and dependency ordering, and writes the results +as a JSON data file and a ready-to-run Python script. Invoke via +``m decompose run --help`` for full option documentation. +""" + import typer # from .inference import app as inference_app diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index bcf1cca9d..efb8f112c 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -1,3 +1,12 @@ +"""Implementation of the ``m decompose run`` CLI command. + +Accepts a task prompt (from a text file or interactive input), calls the multi-step +LLM decomposition pipeline to produce a structured list of subtasks each with +constraints and inter-subtask dependencies, then validates and topologically reorders +the subtasks before writing a JSON result file and a rendered Python script to the +specified output directory. +""" + import json import keyword import re diff --git a/cli/decompose/pipeline.py b/cli/decompose/pipeline.py index 0b44ad41b..1b760697b 100644 --- a/cli/decompose/pipeline.py +++ b/cli/decompose/pipeline.py @@ -1,3 +1,12 @@ +"""Core decomposition pipeline that breaks a task prompt into structured subtasks. + +Provides the ``decompose()`` function, which orchestrates a series of LLM calls +(subtask listing, constraint extraction, validation strategy selection, prompt +generation, and constraint assignment) to produce a ``DecompPipelineResult`` +containing subtasks, per-subtask prompts, constraints, and dependency information. +Supports Ollama, OpenAI-compatible, and RITS inference backends. +""" + import re from enum import StrEnum from typing import Literal, NotRequired, TypedDict diff --git a/cli/decompose/utils.py b/cli/decompose/utils.py index cc879bc1d..15bbd3011 100644 --- a/cli/decompose/utils.py +++ b/cli/decompose/utils.py @@ -1,3 +1,12 @@ +"""Filename validation utilities for the decompose pipeline. + +Provides ``validate_filename``, which checks that a candidate output filename +contains only safe characters (alphanumeric, underscores, hyphens, periods, and +spaces) and falls within a reasonable length limit. Used to prevent path-traversal +or shell-injection issues when writing decomposition output files. +""" + + def validate_filename(candidate_str: str) -> bool: import re diff --git a/cli/eval/__init__.py b/cli/eval/__init__.py index 7f625a26d..cb57f3230 100644 --- a/cli/eval/__init__.py +++ b/cli/eval/__init__.py @@ -1 +1,8 @@ -"""CLI for test-based evaluation""" +"""CLI package for test-based LLM evaluation. + +Provides the ``m eval`` command group, which orchestrates running a generator model +against structured test files and scoring each response with a judge model. Each test +file specifies a set of instructions and input examples; results — including per-input +pass/fail judgements and cumulative pass rates — are written to JSON or JSONL for +downstream analysis. +""" diff --git a/cli/eval/runner.py b/cli/eval/runner.py index 3aface948..2cd5d22c7 100644 --- a/cli/eval/runner.py +++ b/cli/eval/runner.py @@ -1,3 +1,11 @@ +"""Execution engine for the test-based LLM evaluation pipeline. + +Loads JSON test files into ``TestBasedEval`` objects and, for each test, runs a +generator model to produce responses and a separate judge model to score them. Parses +the judge output for a ``{"score": ..., "justification": ...}`` JSON fragment, +aggregates per-input pass/fail counts, and saves the full results to JSON or JSONL. +""" + import json import re from pathlib import Path diff --git a/cli/m.py b/cli/m.py index ab39440eb..b6ea7d95b 100644 --- a/cli/m.py +++ b/cli/m.py @@ -1,4 +1,10 @@ -"""Entrypoint for the M CLI.""" +"""Entrypoint for the ``m`` command-line tool. + +Wires together all CLI sub-applications into a single Typer root command: ``m serve`` +(start a model-serving endpoint), ``m alora`` (train and upload LoRA/aLoRA adapters), +``m decompose`` (LLM-driven task decomposition), and ``m eval`` (test-based model +evaluation). Run ``m --help`` to see all available sub-commands. +""" import typer diff --git a/mellea/backends/__init__.py b/mellea/backends/__init__.py index 9dd455184..65e8d1e63 100644 --- a/mellea/backends/__init__.py +++ b/mellea/backends/__init__.py @@ -1,4 +1,12 @@ -"""Backend implementations.""" +"""Backend implementations for the mellea inference layer. + +This package exposes the concrete machinery for connecting mellea to language model +servers. It bundles ``FormatterBackend`` (a prompt-engineering base class for legacy +models), ``ModelIdentifier`` (portable cross-platform model names), ``ModelOption`` +(generation parameters such as token limits), ``SimpleLRUCache`` (KV-cache +management), and ``MelleaTool`` / ``tool`` (LLM tool definitions). Reach for this +package when configuring a backend, declaring tools, or tuning inference options. +""" # Import from core for ergonomics. from ..core import Backend, BaseModelSubclass diff --git a/mellea/backends/adapters/adapter.py b/mellea/backends/adapters/adapter.py index ead24799c..d1698fffc 100644 --- a/mellea/backends/adapters/adapter.py +++ b/mellea/backends/adapters/adapter.py @@ -1,4 +1,12 @@ -"""Module for adapters to backends.""" +"""Adapter classes for adding fine-tuned modules to inference backends. + +Defines the abstract ``Adapter`` base class and its concrete subclasses +``LocalHFAdapter`` (for locally loaded HuggingFace models) and ``IntrinsicAdapter`` +(for adapters whose metadata is stored in Mellea's intrinsic catalog). Also provides +``get_adapter_for_intrinsic`` for resolving the right adapter class given an +intrinsic name, and ``AdapterMixin`` for backends that support runtime adapter +loading and unloading. +""" import abc import pathlib diff --git a/mellea/backends/backend.py b/mellea/backends/backend.py index 210a70aa4..79b21c844 100644 --- a/mellea/backends/backend.py +++ b/mellea/backends/backend.py @@ -1,4 +1,11 @@ -"""FormatterBackend.""" +"""``FormatterBackend``: base class for prompt-engineering backends. + +``FormatterBackend`` extends the abstract ``Backend`` with a ``ChatFormatter`` and +a ``ModelIdentifier``, bridging mellea's generative programming primitives to models +that do not yet natively support spans or structured fine-tuning. Concrete backend +implementations (e.g. Ollama, HuggingFace, OpenAI) subclass ``FormatterBackend`` and +supply the model-specific ``generate_from_context`` logic. +""" import abc from enum import Enum diff --git a/mellea/backends/cache.py b/mellea/backends/cache.py index 2f42a247e..c2cf2c16e 100644 --- a/mellea/backends/cache.py +++ b/mellea/backends/cache.py @@ -1,4 +1,11 @@ -"""Caching strategies.""" +"""Cache abstractions and implementations for model state. + +Defines the abstract ``Cache`` interface with ``put``, ``get``, and +``current_size`` methods, and provides a concrete ``SimpleLRUCache`` that evicts +the least-recently-used entry when capacity is exceeded — optionally calling an +``on_evict`` callback (e.g. to free GPU memory). Used by local HuggingFace backends +to store and reuse KV cache state across requests. +""" import abc from collections import OrderedDict diff --git a/mellea/backends/kv_block_helpers.py b/mellea/backends/kv_block_helpers.py index 4d63093fb..d31a64281 100644 --- a/mellea/backends/kv_block_helpers.py +++ b/mellea/backends/kv_block_helpers.py @@ -1,4 +1,11 @@ -"""Utilities for KV smashing.""" +"""Low-level utilities for concatenating transformer KV caches (KV smashing). + +Provides functions for merging ``DynamicCache`` and legacy tuple caches along the +time axis (``merge_dynamic_caches``, ``legacy_cache_smash``), and +``tokens_to_legacy_cache`` for converting a tokenized prompt into a prefilled KV +cache. These helpers are used internally by local HuggingFace backends that reuse +cached prefix computations across multiple generation calls. +""" from collections.abc import Iterable from functools import reduce diff --git a/mellea/backends/model_ids.py b/mellea/backends/model_ids.py index 76ba1e5bb..d13bcaeb3 100644 --- a/mellea/backends/model_ids.py +++ b/mellea/backends/model_ids.py @@ -1,4 +1,11 @@ -"""Dataclasses for ModelIdentifiers.""" +"""``ModelIdentifier`` dataclass and a catalog of pre-defined model IDs. + +``ModelIdentifier`` is a frozen dataclass that groups the platform-specific name +variants for a model (HuggingFace, Ollama, WatsonX, MLX, OpenAI, Bedrock) so that +a single constant can be passed to any backend without manual string translation. +The module also ships a curated catalog of ready-to-use constants for popular +open-weight models including IBM Granite 4, Meta Llama 4, Mistral, and Qwen families. +""" import dataclasses diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 2aed992c3..62bb37ef5 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -1,4 +1,11 @@ -"""Utilities for dealing with LLM tools.""" +"""LLM tool definitions, parsing, and validation for mellea backends. + +Provides the ``MelleaTool`` class (and the ``@tool`` decorator shorthand) for +wrapping Python callables as OpenAI-compatible tool schemas, with factory methods +for LangChain and smolagents interoperability. Also includes helpers for converting +tool lists to JSON, extracting tool call requests from raw LLM output strings, and +validating/coercing tool arguments against the tool's JSON schema using Pydantic. +""" import inspect import json diff --git a/mellea/backends/utils.py b/mellea/backends/utils.py index 4104c6502..472bbba4a 100644 --- a/mellea/backends/utils.py +++ b/mellea/backends/utils.py @@ -1,4 +1,11 @@ -"""Utilities for Backends.""" +"""Shared utility functions used across formatter-based backend implementations. + +Provides ``to_chat``, which converts a ``Context`` and a ``Component`` action into +the list of role/content dicts expected by ``apply_chat_template``; and +``to_tool_calls``, which parses a raw model output string into validated +``ModelToolCall`` objects. These helpers are consumed internally by all +``FormatterBackend`` subclasses. +""" from __future__ import annotations diff --git a/mellea/core/__init__.py b/mellea/core/__init__.py index f35b3df78..b031df6ab 100644 --- a/mellea/core/__init__.py +++ b/mellea/core/__init__.py @@ -1,4 +1,13 @@ -"""Core Library for Mellea Interfaces.""" +"""Core abstractions for the mellea library. + +This package defines the fundamental interfaces and data structures on which every +other layer of mellea is built: the ``Backend``, ``Formatter``, and +``SamplingStrategy`` protocols; the ``Component``, ``CBlock``, ``Context``, and +``ModelOutputThunk`` data types that flow through the inference pipeline; and +``Requirement`` / ``ValidationResult`` for constrained generation. Start here when +building a new backend, formatter, or sampling strategy, or when you need the type +definitions shared across the library. +""" from .backend import Backend, BaseModelSubclass, generate_walk from .base import ( diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 6a1a1396a..e08eb15da 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -1,4 +1,11 @@ -"""Interfaces for Backends and Generation.""" +"""Abstract ``Backend`` interface and generation-walk utilities. + +Defines the ``Backend`` abstract base class whose two key abstract methods — +``generate_from_context`` (context-aware single-action generation) and +``generate_from_raw`` (context-free batch generation) — all concrete backends must +implement. Also provides ``generate_walk``, which traverses a ``Component`` tree to +find un-computed ``ModelOutputThunk`` leaves that need to be resolved before rendering. +""" import abc import asyncio diff --git a/mellea/core/base.py b/mellea/core/base.py index 60d650292..10fdb01ef 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -1,4 +1,13 @@ -"""Core Classes and Data Structures.""" +"""Foundational data structures for mellea's generative programming model. + +Defines the building blocks that flow through every layer of the library: ``CBlock`` +(a content block wrapping a string value), ``Component`` (an abstract composable +generative unit), ``ModelOutputThunk`` (a lazily-evaluated model response), +``Context`` and ``ContextTurn`` (stateful conversation history containers), +``TemplateRepresentation`` (the structured rendering of a component for prompt +templates), ``ImageBlock``, and ``ModelToolCall``. Understanding these types is +the starting point for building custom components or sampling strategies. +""" from __future__ import annotations diff --git a/mellea/core/formatter.py b/mellea/core/formatter.py index bed99a052..40bba08e2 100644 --- a/mellea/core/formatter.py +++ b/mellea/core/formatter.py @@ -1,4 +1,11 @@ -"""Interfaces for Formatters.""" +"""Abstract ``Formatter`` interface for rendering components to strings. + +A ``Formatter`` converts ``Component`` and ``CBlock`` objects into the text strings +fed to language model prompts. The single abstract method ``print`` encapsulates this +rendering contract; concrete subclasses such as ``ChatFormatter`` and +``TemplateFormatter`` extend it with chat-message and Jinja2-template rendering +respectively. +""" import abc diff --git a/mellea/core/requirement.py b/mellea/core/requirement.py index feb0559f6..bb58cd815 100644 --- a/mellea/core/requirement.py +++ b/mellea/core/requirement.py @@ -1,4 +1,12 @@ -"""Interface for Requirements.""" +"""``Requirement`` interface for constrained and validated generation. + +A ``Requirement`` pairs a human-readable description with a validation function that +inspects a ``Context`` (and optionally a backend) to determine whether a model output +meets a constraint. ``ValidationResult`` carries the pass/fail verdict along with an +optional reason, score, and the ``ModelOutputThunk`` produced during validation. +Helper factories such as ``default_output_to_bool`` make it easy to build requirements +without boilerplate. +""" import re from collections.abc import Callable diff --git a/mellea/core/sampling.py b/mellea/core/sampling.py index 41b945e0f..05063fdfa 100644 --- a/mellea/core/sampling.py +++ b/mellea/core/sampling.py @@ -1,4 +1,12 @@ -"""Interfaces for Sampling Strategies.""" +"""Abstract interfaces for sampling strategies and their results. + +``SamplingStrategy`` defines the contract for all sampling algorithms: an async +``sample`` method that takes an action, context, backend, and requirements, and +returns a ``SamplingResult``. ``SamplingResult`` records the chosen generation +alongside the full history of intermediate samples, their validation outcomes, +and associated contexts — enabling detailed post-hoc inspection of the sampling +process. +""" import abc from typing import Generic diff --git a/mellea/core/utils.py b/mellea/core/utils.py index 4d1bfdd66..97249cccd 100644 --- a/mellea/core/utils.py +++ b/mellea/core/utils.py @@ -1,4 +1,10 @@ -"""Utils for Core Library.""" +"""Logging utilities for the mellea core library. + +Provides ``FancyLogger``, a singleton logger with colour-coded console output and +an optional REST handler (``RESTHandler``) that forwards log records to a local +``/api/receive`` endpoint when the ``FLOG`` environment variable is set. All +internal mellea modules obtain their logger via ``FancyLogger.get_logger()``. +""" import json import logging diff --git a/mellea/formatters/__init__.py b/mellea/formatters/__init__.py index de782222f..c7489dc31 100644 --- a/mellea/formatters/__init__.py +++ b/mellea/formatters/__init__.py @@ -1,4 +1,12 @@ -"""Formatters.""" +"""Formatters for converting components into model-ready prompts. + +Formatters translate ``Component`` objects into the prompt strings or chat message +lists that inference backends consume. This package exports the abstract ``Formatter`` +interface and two concrete implementations: ``ChatFormatter``, which converts +components into role-labelled chat messages, and ``TemplateFormatter``, which renders +them through Jinja2 templates. Pass a formatter when constructing a +``FormatterBackend`` for your chosen model. +""" # Import from core for ergonomics. from ..core import Formatter diff --git a/mellea/formatters/chat_formatter.py b/mellea/formatters/chat_formatter.py index 4f0848810..d930fde71 100644 --- a/mellea/formatters/chat_formatter.py +++ b/mellea/formatters/chat_formatter.py @@ -1,4 +1,12 @@ -"""ChatFormatter.""" +"""``ChatFormatter`` for converting context histories to chat-message lists. + +``ChatFormatter`` is the standard formatter used by mellea's legacy backends. Its +``to_chat_messages`` method linearises a sequence of ``Component`` and ``CBlock`` +objects into ``Message`` objects with ``user``, ``assistant``, or ``tool`` roles, +handling ``ModelOutputThunk`` responses, image attachments, and parsed structured +outputs. Concrete backends call this formatter when preparing input for a chat +completion endpoint. +""" from ..core import ( CBlock, diff --git a/mellea/formatters/granite/base/optional.py b/mellea/formatters/granite/base/optional.py index 96a151c94..81c42018b 100644 --- a/mellea/formatters/granite/base/optional.py +++ b/mellea/formatters/granite/base/optional.py @@ -1,6 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 -"""Utilities for optional dependencies.""" +"""Context-manager helpers for gracefully handling optional import dependencies. + +Provides ``import_optional``, a context manager that catches ``ImportError`` and +re-raises it with a human-readable install hint (e.g. ``pip install [extra]``), +and ``nltk_check``, a variant tailored to NLTK data-download errors. Used by Granite +formatter modules that have optional third-party dependencies. +""" # Standard import logging diff --git a/mellea/formatters/granite/base/types.py b/mellea/formatters/granite/base/types.py index 7e0a299e7..8446b2525 100644 --- a/mellea/formatters/granite/base/types.py +++ b/mellea/formatters/granite/base/types.py @@ -1,6 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 -"""Common shared types.""" +"""Common Pydantic types shared across the Granite formatter package. + +Defines reusable Pydantic models and mixins, including ``NoDefaultsMixin`` (which +suppresses unset default fields from serialized JSON output) and message/request +types for Granite model chat completions (``ChatMessage``, ``ChatCompletion``, +``VLLMExtraBody``, ``ChatCompletionLogProbs``, and related classes). These types are +consumed internally by the Granite intrinsic formatters. +""" # Standard from typing import Annotated, Any, Literal, TypeAlias diff --git a/mellea/formatters/granite/intrinsics/json_util.py b/mellea/formatters/granite/intrinsics/json_util.py index afd74c4aa..2868451f8 100644 --- a/mellea/formatters/granite/intrinsics/json_util.py +++ b/mellea/formatters/granite/intrinsics/json_util.py @@ -1,6 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 -"""JSON parsing code used by this package.""" +"""JSON parsing utilities for Granite intrinsic formatters. + +Provides a fast, position-aware JSON literal parser (``JsonLiteralWithPosition``) used +to extract and re-score tokens inside structured model outputs. The module also defines +compiled regular expressions for JSON structural characters, numbers, booleans, and +null values that are used throughout the Granite intrinsic formatting pipeline. +""" # Standard import bisect diff --git a/mellea/formatters/template_formatter.py b/mellea/formatters/template_formatter.py index c15e7ed76..694d0919c 100644 --- a/mellea/formatters/template_formatter.py +++ b/mellea/formatters/template_formatter.py @@ -1,4 +1,12 @@ -"""Template Formatter.""" +"""``TemplateFormatter``: Jinja2-template-based formatter for legacy backends. + +``TemplateFormatter`` extends ``ChatFormatter`` to look up a per-component Jinja2 +template at rendering time, allowing each ``Component`` type to control its own +prompt representation. Template discovery walks a configurable ``template_path`` and +the built-in templates directory; results are cached in a ``SimpleLRUCache`` for +performance. Use this formatter when your backend requires hand-crafted prompts +rather than a generic chat-message rendering. +""" import os import re diff --git a/mellea/helpers/__init__.py b/mellea/helpers/__init__.py index bc8a10642..62b22eb6a 100644 --- a/mellea/helpers/__init__.py +++ b/mellea/helpers/__init__.py @@ -1,4 +1,13 @@ -"""Various Helpers and Utilities.""" +"""Low-level helpers and utilities supporting mellea backends. + +This package provides the internal plumbing used by the built-in backend +implementations: async utilities (``send_to_queue``, ``wait_for_all_mots``, +``ClientCache``) for managing concurrent model-output thunks; OpenAI-compatible +message conversion helpers (``message_to_openai_message``, ``messages_to_docs``, +``chat_completion_delta_merge``); and ``_ServerType`` detection for adapting +structured-output support to the target server. Most user code will not import from +this package directly — it is consumed internally by the backend layer. +""" from .async_helpers import ( ClientCache, diff --git a/mellea/helpers/async_helpers.py b/mellea/helpers/async_helpers.py index 3bec74a5c..c3bfc86a0 100644 --- a/mellea/helpers/async_helpers.py +++ b/mellea/helpers/async_helpers.py @@ -1,4 +1,12 @@ -"""Async helper functions.""" +"""Async helper functions for managing concurrent model output thunks. + +Provides ``send_to_queue``, which feeds a backend response coroutine or async iterator +into an ``asyncio.Queue`` (including sentinel and error forwarding); ``wait_for_all_mots``, +which gathers multiple ``ModelOutputThunk`` computations in a single ``asyncio.gather`` +call; and ``get_current_event_loop``, a safe wrapper that returns ``None`` instead of +raising when no event loop is running. These utilities are used internally by backends +that operate in async contexts. +""" import asyncio from collections import OrderedDict diff --git a/mellea/helpers/server_type.py b/mellea/helpers/server_type.py index 549a0a046..93c60a0c0 100644 --- a/mellea/helpers/server_type.py +++ b/mellea/helpers/server_type.py @@ -1,4 +1,12 @@ -"""Server Type Helpers.""" +"""Utilities for detecting and classifying the target inference server. + +Defines the ``_ServerType`` enum (``LOCALHOST``, ``OPENAI``, ``REMOTE_VLLM``, +``UNKNOWN``) and ``_server_type``, which classifies a URL by hostname. Also provides +``is_vllm_server_with_structured_output``, which probes a server's ``/version`` +endpoint to determine whether it supports the ``structured_outputs`` parameter +introduced in vLLM ≥ 0.12.0. Used by the OpenAI-compatible backend to choose between +``guided_json`` and ``structured_outputs`` request formats. +""" import json from collections.abc import Mapping diff --git a/mellea/stdlib/__init__.py b/mellea/stdlib/__init__.py index 35fce96fa..a402d5733 100644 --- a/mellea/stdlib/__init__.py +++ b/mellea/stdlib/__init__.py @@ -1 +1,11 @@ -"""The mellea standard library.""" +"""The mellea standard library of components, sessions, and sampling strategies. + +This package provides the high-level building blocks for writing generative programs +with mellea. It contains ready-to-use ``Component`` types (``Instruction``, +``Message``, ``Document``, ``Intrinsic``, ``SimpleComponent``, and more), context +implementations (``ChatContext``, ``SimpleContext``), sampling strategies (rejection +sampling, budget forcing), session management via ``MelleaSession``, and the +``@mify`` decorator for turning ordinary Python objects into components. Import from +the sub-packages — ``mellea.stdlib.components``, ``mellea.stdlib.sampling``, and +``mellea.stdlib.session`` — for day-to-day use. +""" diff --git a/mellea/stdlib/components/chat.py b/mellea/stdlib/components/chat.py index 22c11369d..3f199f3f0 100644 --- a/mellea/stdlib/components/chat.py +++ b/mellea/stdlib/components/chat.py @@ -1,4 +1,12 @@ -"""Chat primitives.""" +"""Chat primitives: the ``Message`` and ``ToolMessage`` components. + +Defines ``Message``, the ``Component`` subtype used to represent a single turn in a +chat history with a ``role`` (``user``, ``assistant``, ``system``, or ``tool``), +text ``content``, and optional ``images`` and ``documents`` attachments. Also provides +``ToolMessage`` (a ``Message`` subclass that carries the tool name and arguments) and +the ``as_chat_history`` utility for converting a ``Context`` into a flat list of +``Message`` objects. +""" from collections.abc import Mapping from typing import Any, Literal diff --git a/mellea/stdlib/components/docs/document.py b/mellea/stdlib/components/docs/document.py index e998a9fcc..21e572c12 100644 --- a/mellea/stdlib/components/docs/document.py +++ b/mellea/stdlib/components/docs/document.py @@ -1,4 +1,10 @@ -"""Document component.""" +"""``Document`` component for grounding model inputs with text passages. + +``Document`` wraps a text passage with an optional ``title`` and ``doc_id``, and +renders them inline as a formatted citation string for the model. Documents are +typically attached to a ``Message`` via its ``documents`` parameter, enabling +retrieval-augmented generation (RAG) workflows. +""" from ....core import CBlock, Component, ModelOutputThunk diff --git a/mellea/stdlib/components/docs/richdocument.py b/mellea/stdlib/components/docs/richdocument.py index 67bcb01fa..2cfe06970 100644 --- a/mellea/stdlib/components/docs/richdocument.py +++ b/mellea/stdlib/components/docs/richdocument.py @@ -1,4 +1,12 @@ -"""Representations of Docling Documents.""" +"""``RichDocument``, ``Table``, and related helpers backed by Docling. + +``RichDocument`` wraps a ``DoclingDocument`` (e.g. produced by converting a PDF or +Markdown file) and renders it as Markdown for a language model. ``Table`` represents a +single table within a Docling document and provides ``transpose``, ``to_markdown``, and +query/transform helpers. Use ``RichDocument.from_document_file`` to convert a PDF or +other supported format, and ``get_tables()`` to extract structured table data for +downstream LLM-driven Q&A or transformation tasks. +""" from __future__ import annotations diff --git a/mellea/stdlib/components/instruction.py b/mellea/stdlib/components/instruction.py index 288d9e2d8..db03293f0 100644 --- a/mellea/stdlib/components/instruction.py +++ b/mellea/stdlib/components/instruction.py @@ -1,4 +1,12 @@ -"""Instructions.""" +"""``Instruction`` component for instruct/validate/repair loops. + +``Instruction`` is the primary component type used with ``MelleaSession.instruct``. It +packages a task ``description``, a list of ``Requirement`` constraints, optional +in-context-learning examples, a grounding context dict, user variables for Jinja2 +template interpolation, and output/input prefix overrides into a single renderable +unit. The session's sampling strategy evaluates each requirement against the model's +output and may repair or resample until all requirements pass. +""" from __future__ import annotations diff --git a/mellea/stdlib/components/intrinsic/intrinsic.py b/mellea/stdlib/components/intrinsic/intrinsic.py index c12fa54fe..d8cc1e032 100644 --- a/mellea/stdlib/components/intrinsic/intrinsic.py +++ b/mellea/stdlib/components/intrinsic/intrinsic.py @@ -1,4 +1,11 @@ -"""Module for Intrinsics.""" +"""``Intrinsic`` component for invoking fine-tuned adapter capabilities. + +An ``Intrinsic`` component references a named adapter from Mellea's intrinsic catalog +and transforms a chat completion request — typically by injecting new messages, +modifying model parameters, or applying structured output constraints. It must be +paired with a backend that supports adapter loading (e.g. ``LocalHFBackend`` with an +attached ``IntrinsicAdapter``). +""" from ....backends.adapters import AdapterType, fetch_intrinsic_metadata from ....core import CBlock, Component, ModelOutputThunk, TemplateRepresentation diff --git a/mellea/stdlib/components/mify.py b/mellea/stdlib/components/mify.py index c54f16056..01516dfa7 100644 --- a/mellea/stdlib/components/mify.py +++ b/mellea/stdlib/components/mify.py @@ -1,4 +1,13 @@ -"""Mify classes and objects.""" +"""The ``@mify`` decorator for turning Python objects into ``Component``s. + +``mify`` wraps an existing Python class or instance with the ``MifiedProtocol`` +interface, exposing its fields as named spans and its documented methods as +``MelleaTool`` instances callable by the LLM. The resulting ``MifiedProtocol`` object +can be queried, transformed, and formatted for a language model without any manual +``Component`` subclassing. Use ``mify`` when you have an existing domain object +(dataclass, Pydantic model, or plain class) that you want to expose directly to an +LLM-driven pipeline. +""" import inspect import types diff --git a/mellea/stdlib/components/mobject.py b/mellea/stdlib/components/mobject.py index b5dc25f87..3df8e8a72 100644 --- a/mellea/stdlib/components/mobject.py +++ b/mellea/stdlib/components/mobject.py @@ -1,4 +1,12 @@ -"""MObject.""" +"""``MObject``, ``Query``, ``Transform``, and ``MObjectProtocol`` for query/transform workflows. + +Defines the ``MObjectProtocol`` protocol for objects that can be queried and +transformed by an LLM, and the concrete ``MObject`` base class that implements it. +Also provides the ``Query`` and ``Transform`` ``Component`` subtypes, which wrap an +object with a natural-language question or mutation instruction respectively. These +primitives underpin ``@mify`` and can be composed directly to build document Q&A +or structured extraction pipelines. +""" from __future__ import annotations diff --git a/mellea/stdlib/components/react.py b/mellea/stdlib/components/react.py index 55af32a9a..845432313 100644 --- a/mellea/stdlib/components/react.py +++ b/mellea/stdlib/components/react.py @@ -1,4 +1,12 @@ -"""Components used for ReACT.""" +"""Components that implement the ReACT (Reason + Act) agentic pattern. + +Provides ``ReactInitiator``, which primes the model with a goal and a tool list, and +``ReactThought``, which signals a thinking step. Also exports the +``MELLEA_FINALIZER_TOOL`` sentinel string used to signal loop termination. These +components are consumed by ``mellea.stdlib.frameworks.react``, which orchestrates the +reasoning–acting cycle until the model invokes ``final_answer`` or the step budget +is exhausted. +""" import inspect from typing import Generic diff --git a/mellea/stdlib/components/simple.py b/mellea/stdlib/components/simple.py index 495c9eb32..191e7c515 100644 --- a/mellea/stdlib/components/simple.py +++ b/mellea/stdlib/components/simple.py @@ -1,4 +1,10 @@ -"""SimpleComponent.""" +"""``SimpleComponent``: a lightweight named-span component. + +``SimpleComponent`` accepts arbitrary keyword arguments (strings, ``CBlock``s, or +``Component``s) and renders them as a JSON object keyed by the argument names. It is +the go-to component type for ad-hoc prompts that do not require a dedicated +``Component`` subclass or a Jinja2 template. +""" from typing import Any diff --git a/mellea/stdlib/context.py b/mellea/stdlib/context.py index 7f4b0da79..fc55605e3 100644 --- a/mellea/stdlib/context.py +++ b/mellea/stdlib/context.py @@ -1,4 +1,11 @@ -"""Basic Contexts.""" +"""Concrete ``Context`` implementations for common conversation patterns. + +Provides ``ChatContext``, which accumulates all turns in a sliding-window chat history +(configurable via ``window_size``), and ``SimpleContext``, in which each interaction +is treated as a stateless single-turn exchange (no prior history is passed to the +model). Import ``ChatContext`` for multi-turn conversations and ``SimpleContext`` when +you want each call to the model to be independent. +""" from __future__ import annotations diff --git a/mellea/stdlib/frameworks/react.py b/mellea/stdlib/frameworks/react.py index 059c12977..810542295 100644 --- a/mellea/stdlib/frameworks/react.py +++ b/mellea/stdlib/frameworks/react.py @@ -1,4 +1,11 @@ -"""ReACT Agentic Pattern.""" +"""ReACT (Reason + Act) agentic pattern implementation. + +Provides the ``react()`` async function, which drives a tool-use loop: the model +reasons about a goal, selects a tool, receives the result as an observation, and +repeats until it calls ``final_answer`` or the ``loop_budget`` is exhausted. Accepts +any list of ``AbstractMelleaTool`` instances and a ``ChatContext`` for multi-turn +history tracking. Raises ``RuntimeError`` if the loop ends without a final answer. +""" # from PIL import Image as PILImage from mellea.backends.model_options import ModelOption diff --git a/mellea/stdlib/requirements/tool_reqs.py b/mellea/stdlib/requirements/tool_reqs.py index 8def8bf16..13471ae03 100644 --- a/mellea/stdlib/requirements/tool_reqs.py +++ b/mellea/stdlib/requirements/tool_reqs.py @@ -1,4 +1,12 @@ -"""Requirements for tool-use workflows.""" +"""``Requirement`` factories for tool-use validation. + +Provides ``uses_tool``, a ``Requirement`` factory that validates whether a model +response includes a call to a specified tool — useful when you need to enforce tool +invocation via rejection sampling rather than relying solely on the model's +``tool_choice`` setting. Also provides ``tool_arg_validator``, which validates the +value of a specific argument to a named tool. Both accept either the tool's string +name or its callable. +""" from collections.abc import Callable diff --git a/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py b/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py index 12ea3f5a0..86e925486 100644 --- a/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py +++ b/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py @@ -1,4 +1,12 @@ -"""Budget forcing implementation.""" +"""Budget-forcing generation algorithm for thinking models. + +Implements ``think_budget_forcing``, which extends a model's reasoning phase by +repeatedly appending a "think more" suffix whenever the model attempts to close its +```` block prematurely, following the method proposed in arXiv:2501.19393. +Generation is split into a thinking pass (bounded by ``think_max_tokens``) and an +answer pass (bounded by ``answer_max_tokens``), using the raw completions API of an +``OllamaModelBackend``. +""" from typing import Any diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 784923d9f..eae7b24d3 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -1,4 +1,12 @@ -"""Mellea Sessions.""" +"""``MelleaSession``: the primary entry point for running generative programs. + +``MelleaSession`` wraps a ``Backend`` and a ``Context`` and exposes high-level methods +(``act``, ``instruct``, ``sample``) that drive the generate–validate–repair loop. It +also manages a global context variable (accessible via ``get_session()``) so that +nested components can reach the current session without explicit threading. Use +``start_session(...)`` as a context manager to create and automatically clean up a +session. +""" from __future__ import annotations diff --git a/mellea/stdlib/tools/interpreter.py b/mellea/stdlib/tools/interpreter.py index 622e19305..166ef0b39 100644 --- a/mellea/stdlib/tools/interpreter.py +++ b/mellea/stdlib/tools/interpreter.py @@ -1,4 +1,14 @@ -"""Code interpreter tool.""" +"""Code interpreter tool and execution environments for agentic workflows. + +Provides ``ExecutionResult`` (capturing stdout, stderr, success, and optional static +analysis output) and three concrete ``ExecutionEnvironment`` implementations: +``StaticAnalysisEnvironment`` (parse and import-check only, no execution), +``UnsafeEnvironment`` (subprocess execution in the current Python environment), and +``LLMSandboxEnvironment`` (Docker-isolated execution via ``llm-sandbox``). All +environments support an optional ``allowed_imports`` allowlist. The top-level +``code_interpreter`` and ``local_code_interpreter`` functions are ready to be wrapped +as ``MelleaTool`` instances for ReACT or other agentic loops. +""" import ast import subprocess From 50701d31d4fc0600a40afbc8542e9ca12bdd6d7d Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Tue, 10 Mar 2026 23:08:35 +0000 Subject: [PATCH 02/14] fix: replace EN DASH with hyphen in docstrings (RUF002) --- mellea/stdlib/components/react.py | 2 +- mellea/stdlib/session.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mellea/stdlib/components/react.py b/mellea/stdlib/components/react.py index 845432313..caa28a466 100644 --- a/mellea/stdlib/components/react.py +++ b/mellea/stdlib/components/react.py @@ -4,7 +4,7 @@ ``ReactThought``, which signals a thinking step. Also exports the ``MELLEA_FINALIZER_TOOL`` sentinel string used to signal loop termination. These components are consumed by ``mellea.stdlib.frameworks.react``, which orchestrates the -reasoning–acting cycle until the model invokes ``final_answer`` or the step budget +reasoning-acting cycle until the model invokes ``final_answer`` or the step budget is exhausted. """ diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index eae7b24d3..04d8c112e 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -1,7 +1,7 @@ """``MelleaSession``: the primary entry point for running generative programs. ``MelleaSession`` wraps a ``Backend`` and a ``Context`` and exposes high-level methods -(``act``, ``instruct``, ``sample``) that drive the generate–validate–repair loop. It +(``act``, ``instruct``, ``sample``) that drive the generate-validate-repair loop. It also manages a global context variable (accessible via ``get_session()``) so that nested components can reach the current session without explicit threading. Use ``start_session(...)`` as a context manager to create and automatically clean up a From 98ab258474d2d34d9d2f1680691cea813edd5052 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Wed, 11 Mar 2026 07:39:40 +0000 Subject: [PATCH 03/14] docs: add Google-style Args/Returns to public functions (#613) Add missing ``Args:`` and ``Returns:`` sections to all public functions and methods across ``mellea/`` and ``cli/`` that lacked them. Also converts RST-style ``:param:``/``:returns:`` docstrings inherited from the ``granite_io`` upstream into Google style to match the project convention (``pyproject.toml`` ``convention = "google"``). --- cli/alora/commands.py | 39 ++++- cli/alora/readme_generator.py | 29 +++- cli/eval/runner.py | 79 ++++++++-- mellea/backends/adapters/catalog.py | 12 +- mellea/backends/bedrock.py | 32 +++- mellea/backends/kv_block_helpers.py | 31 +++- mellea/backends/tools.py | 60 +++++++- mellea/backends/utils.py | 21 ++- mellea/core/requirement.py | 12 +- mellea/formatters/granite/base/optional.py | 10 +- mellea/formatters/granite/base/util.py | 76 ++++++--- mellea/formatters/granite/granite3/output.py | 35 +++++ mellea/formatters/granite/intrinsics/input.py | 40 +++-- .../granite/intrinsics/json_util.py | 103 ++++++++----- mellea/formatters/granite/intrinsics/util.py | 60 +++++--- .../granite/retrievers/embeddings.py | 62 +++++--- mellea/formatters/granite/retrievers/util.py | 29 ++-- mellea/helpers/async_helpers.py | 42 ++++- mellea/helpers/openai_compatible_helpers.py | 26 +++- mellea/helpers/server_type.py | 7 +- mellea/stdlib/components/chat.py | 10 +- mellea/stdlib/components/intrinsic/rag.py | 144 +++++++++--------- mellea/stdlib/functional.py | 66 +++++++- mellea/stdlib/requirements/md.py | 10 +- mellea/stdlib/requirements/requirement.py | 34 ++++- mellea/stdlib/requirements/tool_reqs.py | 8 +- .../sampling_algos/budget_forcing_alg.py | 31 ++-- mellea/stdlib/session.py | 79 +++++++++- mellea/stdlib/tools/interpreter.py | 6 + mellea/telemetry/tracing.py | 14 +- 30 files changed, 919 insertions(+), 288 deletions(-) diff --git a/cli/alora/commands.py b/cli/alora/commands.py index c39d807a9..c3cd041fc 100644 --- a/cli/alora/commands.py +++ b/cli/alora/commands.py @@ -31,7 +31,21 @@ def alora_train( max_length: int = typer.Option(1024, help="Max sequence length"), grad_accum: int = typer.Option(4, help="Gradient accumulation steps"), ): - """Train an aLoRA or LoRA model on your dataset.""" + """Train an aLoRA or LoRA model on your dataset. + + Args: + datafile: JSONL file with item/label pairs for training. + basemodel: Base model ID or path. + outfile: Path to save adapter weights. + promptfile: Path to load the prompt format file. + adapter: Adapter type; ``"alora"`` or ``"lora"``. + device: Device to train on: ``"auto"``, ``"cpu"``, ``"cuda"``, or ``"mps"``. + epochs: Number of training epochs. + learning_rate: Learning rate for the optimizer. + batch_size: Per-device training batch size. + max_length: Maximum sequence length. + grad_accum: Number of gradient accumulation steps. + """ from cli.alora.train import train_model train_model( @@ -65,7 +79,17 @@ def alora_upload( "processing if the model is invoked as an intrinsic.", ), ): - """Upload trained adapter to remote model registry.""" + """Upload trained adapter to remote model registry. + + Args: + weight_path: Path to saved adapter weights directory. + name: Destination model name on Hugging Face Hub + (e.g. ``"acme/carbchecker-alora"``). + intrinsic: If ``True``, the adapter implements an intrinsic and an + ``io.yaml`` file must also be provided. + io_yaml: Path to the ``io.yaml`` file configuring input/output processing + when the model is invoked as an intrinsic. + """ from cli.alora.intrinsic_uploader import upload_intrinsic from cli.alora.upload import upload_model @@ -114,7 +138,16 @@ def alora_add_readme( "processing if the model is invoked as an intrinsic.", ), ): - """Generate and upload an INTRINSIC_README.md for a trained adapter.""" + """Generate and upload an INTRINSIC_README.md for a trained adapter. + + Args: + datafile: JSONL file with item/label pairs used to train the adapter. + basemodel: Base model ID or path. + promptfile: Path to the prompt format file, or ``None``. + name: Destination model name on Hugging Face Hub. + hints: Path to a file containing additional domain hints, or ``None``. + io_yaml: Path to the ``io.yaml`` intrinsic configuration file, or ``None``. + """ from huggingface_hub import HfFolder, create_repo, upload_file from cli.alora.readme_generator import generate_readme diff --git a/cli/alora/readme_generator.py b/cli/alora/readme_generator.py index 7002bb96c..f0a5e80d4 100644 --- a/cli/alora/readme_generator.py +++ b/cli/alora/readme_generator.py @@ -129,8 +129,20 @@ def make_readme_jinja_dict( """Generate all template variables for the intrinsic README using an LLM. Loads the first five lines of the JSONL dataset, determines the input structure, - and uses m.instruct with deterministic requirements and rejection sampling to + and uses ``m.instruct`` with deterministic requirements and rejection sampling to generate README template variables. + + Args: + m: Active ``MelleaSession`` to use for LLM generation. + dataset_path: Path to the JSONL training dataset file. + base_model: Base model ID or path used to train the adapter. + prompt_file: Path to the prompt format file (empty string if not provided). + name: Destination model name on Hugging Face Hub + (e.g. ``"acme/carbchecker-alora"``). + hints: Optional string of additional domain hints to include in the prompt. + + Returns: + Dict of Jinja2 template variables for rendering the ``INTRINSIC_README.md``. """ # Load first 5 lines of the dataset. samples = [] @@ -270,8 +282,19 @@ def generate_readme( ) -> str: """Generate an INTRINSIC_README.md file from the dataset and template. - Creates a MelleaSession, uses the LLM to generate template variables, - renders the Jinja template, and writes the result to output_path. + Creates a ``MelleaSession``, uses the LLM to generate template variables, + renders the Jinja template, and writes the result to ``output_path``. + + Args: + dataset_path: Path to the JSONL training dataset file. + base_model: Base model ID or path used to train the adapter. + prompt_file: Path to the prompt format file, or ``None``. + output_path: Destination path for the generated README file. + name: Destination model name on Hugging Face Hub. + hints: Optional string of additional domain hints for the LLM. + + Returns: + The path to the written output file (same as ``output_path``). """ from jinja2 import Environment, FileSystemLoader diff --git a/cli/eval/runner.py b/cli/eval/runner.py index 2cd5d22c7..5d2b17c9b 100644 --- a/cli/eval/runner.py +++ b/cli/eval/runner.py @@ -86,7 +86,19 @@ def pass_rate(self) -> float: def create_session( backend: str, model: str | None, max_tokens: int | None ) -> mellea.MelleaSession: - """Create a mellea session with the specified backend and model.""" + """Create a mellea session with the specified backend and model. + + Args: + backend: Backend name: ``"ollama"``, ``"openai"``, ``"hf"``, + ``"watsonx"``, or ``"litellm"``. + model: Model ID or ``ModelIdentifier`` attribute name, or ``None`` + to use the default model. + max_tokens: Maximum number of tokens to generate, or ``None`` for + the backend default. + + Returns: + A configured ``MelleaSession`` ready for generation. + """ model_id = None if model: if model.isupper() or "_" in model: @@ -173,14 +185,25 @@ def run_evaluations( output_format: str, continue_on_error: bool, ): - """Run all 'unit test' evaluations - - Each test file should be a json containing: - "id": an id that is unique to this test file - "source": the origin for the evaluation prompts, else "N/A" - "name": an instruction-following attribute that the user intends to evaluate through this test - "instructions": a set (in string form) of requirements which the generation should follow; the judge will evaluate if these are satisfied - "examples": a list of entries containing an input_id, an input(prompt), and a list of targets. Each input may have multiple (or no) targets; inputs and targets are in messages format. + """Run all unit-test evaluations against a generation model and a judge model. + + Args: + test_files: List of paths to JSON test files. Each file should contain + ``"id"``, ``"source"``, ``"name"``, ``"instructions"``, and + ``"examples"`` fields. + backend: Backend name for the generation model. + model: Model ID for the generator, or ``None`` for the default. + max_gen_tokens: Maximum tokens for the generator, or ``None`` for the + backend default. + judge_backend: Backend name for the judge model, or ``None`` to reuse + the generation backend. + judge_model: Model ID for the judge, or ``None`` for the default. + max_judge_tokens: Maximum tokens for the judge, or ``None`` for the + backend default. + output_path: File path prefix for saving results. + output_format: Output format: ``"json"`` or ``"jsonl"``. + continue_on_error: If ``True``, skip failed test evaluations instead of + raising. """ all_test_evals: list[TestBasedEval] = [] @@ -248,9 +271,18 @@ def execute_test_eval( generation_session: mellea.MelleaSession, judge_session: mellea.MelleaSession, ) -> TestEvalResult: - """Execute a single test evaluation - For each input in the test, generate a response using generation_session - Then, after all inputs are processed, validate using judge_session. + """Execute a single test evaluation. + + For each input in the test, generates a response using ``generation_session``, + then validates using ``judge_session``. + + Args: + test_eval: The ``TestBasedEval`` object containing inputs and targets. + generation_session: ``MelleaSession`` used to produce model responses. + judge_session: ``MelleaSession`` used to score model responses. + + Returns: + A ``TestEvalResult`` with per-input pass/fail outcomes. """ input_results = [] @@ -294,6 +326,16 @@ def execute_test_eval( def parse_judge_output(judge_output: str): + """Parse score and justification from a judge model's output string. + + Args: + judge_output: Raw text output from the judge model. + + Returns: + A ``(score, justification)`` tuple where ``score`` is an integer (or + ``None`` if parsing failed) and ``justification`` is an explanatory + string. + """ try: json_match = re.search(r'\{[^}]*"score"[^}]*\}', judge_output, re.DOTALL) if json_match: @@ -315,6 +357,14 @@ def parse_judge_output(judge_output: str): def save_results(results: list[TestEvalResult], output_path: str, output_format: str): + """Persist evaluation results to disk in JSON or JSONL format. + + Args: + results: List of ``TestEvalResult`` objects to serialise. + output_path: Destination file path (extension may be appended if it + does not match ``output_format``). + output_format: Format string: ``"json"`` or ``"jsonl"``. + """ output_path_obj = Path(output_path) if output_path_obj.suffix != f".{output_format}": output_path_obj = Path(f"{output_path}.{output_format}") @@ -347,6 +397,11 @@ def save_results(results: list[TestEvalResult], output_path: str, output_format: def summary_stats(results: list[TestEvalResult]): + """Print aggregated pass-rate statistics for a set of evaluation results. + + Args: + results: List of ``TestEvalResult`` objects to summarise. + """ total_inputs = sum(r.total_count for r in results) passed_inputs = sum(r.passed_count for r in results) overall_pass_rate = passed_inputs / total_inputs if total_inputs > 0 else 0.0 diff --git a/mellea/backends/adapters/catalog.py b/mellea/backends/adapters/catalog.py index 99d773ca4..2ff376b2a 100644 --- a/mellea/backends/adapters/catalog.py +++ b/mellea/backends/adapters/catalog.py @@ -74,16 +74,22 @@ class IntriniscsCatalogEntry(pydantic.BaseModel): def known_intrinsic_names() -> list[str]: - """:returns: List of all known user-visible names for intrinsics.""" + """Return all known user-visible names for intrinsics. + + Returns: + List of all known user-visible intrinsic names. + """ return list(_INTRINSICS_CATALOG.keys()) def fetch_intrinsic_metadata(intrinsic_name: str) -> IntriniscsCatalogEntry: """Retrieve information about the adapter that backs an intrinsic. - :param intrinsic_name: User-visible name of the intrinsic + Args: + intrinsic_name: User-visible name of the intrinsic. - :returns: Metadata about the adapter(s) that implement the intrinsic. + Returns: + Metadata about the adapter(s) that implement the intrinsic. """ if intrinsic_name not in _INTRINSICS_CATALOG: raise ValueError( diff --git a/mellea/backends/bedrock.py b/mellea/backends/bedrock.py index 414da1ac4..e3ae3012b 100644 --- a/mellea/backends/bedrock.py +++ b/mellea/backends/bedrock.py @@ -22,7 +22,15 @@ def _make_mantle_uri(region: str | None = None): def list_mantle_models(region: str | None = None) -> list: - """Helper function get getting all models available at a bedrock-mantle endpoint.""" + """Return all models available at a bedrock-mantle endpoint. + + Args: + region: AWS region name (e.g. ``"us-east-1"``), or ``None`` to use the + default region. + + Returns: + List of model objects returned by the Bedrock Mantle models API. + """ uri = _make_mantle_uri(region) client = OpenAI(base_url=uri, api_key=os.environ["AWS_BEARER_TOKEN_BEDROCK"]) ms = client.models.list() @@ -32,7 +40,14 @@ def list_mantle_models(region: str | None = None) -> list: def stringify_mantle_model_ids(region: str | None = None) -> str: - """Helper function for getting a human-readable list of all models available at the mantle endpoint for an AWS region.""" + """Return a human-readable list of all models available at the mantle endpoint for an AWS region. + + Args: + region: AWS region name, or ``None`` to use the default region. + + Returns: + Newline-separated string of model IDs prefixed with ``" * "``. + """ models = list_mantle_models() model_names = "\n * ".join([str(m.id) for m in models]) return f" * {model_names}" @@ -41,7 +56,18 @@ def stringify_mantle_model_ids(region: str | None = None) -> str: def create_bedrock_mantle_backend( model_id: ModelIdentifier | str, region: str | None = None ) -> OpenAIBackend: - """Returns an OpenAI backend that points to Bedrock mantle for model `model_id`.""" + """Return an OpenAI backend that points to Bedrock mantle for the given model. + + Args: + model_id: The model to use, either as a ``ModelIdentifier`` (which must have + a ``bedrock_name``) or a raw Bedrock model ID string. + region: AWS region name, or ``None`` to use the default region + (``"us-east-1"``). + + Returns: + An ``OpenAIBackend`` configured to call the specified model via AWS Bedrock + Mantle. + """ model_name = "" match model_id: case ModelIdentifier() if model_id.bedrock_name is None: diff --git a/mellea/backends/kv_block_helpers.py b/mellea/backends/kv_block_helpers.py index d31a64281..4388b1222 100644 --- a/mellea/backends/kv_block_helpers.py +++ b/mellea/backends/kv_block_helpers.py @@ -21,7 +21,15 @@ def legacy_cache_smash(a: LegacyCache, b: LegacyCache) -> LegacyCache: - """Concatenates two LegacyCache Ks and Vs along the time axis.""" + """Concatenates two LegacyCache Ks and Vs along the time axis. + + Args: + a: First legacy KV cache (tuple of per-layer (K, V) tensor pairs). + b: Second legacy KV cache to concatenate after ``a``. + + Returns: + New legacy cache with ``b`` appended to ``a`` along the sequence dimension. + """ legacy_merged = tuple( (torch.cat([a[i][0], b[i][0]], dim=2), torch.cat([a[i][1], b[i][1]], dim=2)) for i in range(len(a)) @@ -30,7 +38,14 @@ def legacy_cache_smash(a: LegacyCache, b: LegacyCache) -> LegacyCache: def merge_dynamic_caches(caches: Iterable[DynamicCache]) -> DynamicCache: - """Merges two DynamicCache Ks and Vs along the time axis.""" + """Merges two DynamicCache Ks and Vs along the time axis. + + Args: + caches: Iterable of ``DynamicCache`` objects to merge in order. + + Returns: + A single ``DynamicCache`` with all caches concatenated along the sequence dimension. + """ legacies = [c.to_legacy_cache() for c in caches] # type: ignore assert len(legacies) >= 1 rv = DynamicCache.from_legacy_cache(reduce(legacy_cache_smash, legacies)) # type: ignore @@ -40,7 +55,17 @@ def merge_dynamic_caches(caches: Iterable[DynamicCache]) -> DynamicCache: def tokens_to_legacy_cache( model: PreTrainedModel, device: str, tokens_or_cache: BatchEncoding | DynamicCache ) -> Iterable[LegacyCache]: - """Prefills and returns Ks and Vs as a LegacyCache.""" + """Prefills and returns Ks and Vs as a LegacyCache. + + Args: + model: The HuggingFace model used for prefill. + device: Target device string (e.g. ``"cuda"``, ``"cpu"``). + tokens_or_cache: Either a ``BatchEncoding`` to prefill, or an existing + ``DynamicCache`` to convert directly. + + Returns: + Legacy KV cache representation as a tuple of per-layer (K, V) tensor pairs. + """ if type(tokens_or_cache) is DynamicCache: return tokens_or_cache.to_legacy_cache() # type: ignore else: diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 62bb37ef5..264fb77b8 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -230,6 +230,11 @@ def add_tools_from_model_options( """If model_options has tools, add those tools to the tools_dict. Accepts MelleaTool instances or @tool decorated functions. + + Args: + tools_dict: Mutable mapping of tool name to tool instance; modified in-place. + model_options: Model options dict that may contain a ``ModelOption.TOOLS`` + entry (either a list of ``MelleaTool`` or a ``dict[str, MelleaTool]``). """ model_opts_tools = model_options.get(ModelOption.TOOLS, None) if model_opts_tools is None: @@ -267,7 +272,13 @@ def add_tools_from_context_actions( tools_dict: dict[str, AbstractMelleaTool], ctx_actions: list[Component | CBlock] | None, ): - """If any of the actions in ctx_actions have tools in their template_representation, add those to the tools_dict.""" + """If any of the actions in ctx_actions have tools in their template_representation, add those to the tools_dict. + + Args: + tools_dict: Mutable mapping of tool name to tool instance; modified in-place. + ctx_actions: List of ``Component`` or ``CBlock`` objects whose template + representations may declare tools, or ``None`` to skip. + """ if ctx_actions is None: return @@ -286,6 +297,12 @@ def add_tools_from_context_actions( def convert_tools_to_json(tools: dict[str, AbstractMelleaTool]) -> list[dict]: """Convert tools to json dict representation. + Args: + tools: Mapping of tool name to ``AbstractMelleaTool`` instance. + + Returns: + List of OpenAI-compatible JSON tool schema dicts, one per tool. + Notes: - Huggingface transformers library lets you pass in an array of functions but doesn't like methods. - WatsonxAI uses `from langchain_ibm.chat_models import convert_to_openai_tool` in their demos, but it gives the same values. @@ -295,7 +312,14 @@ def convert_tools_to_json(tools: dict[str, AbstractMelleaTool]) -> list[dict]: def json_extraction(text: str) -> Generator[dict, None, None]: - """Yields the next valid json object in a given string.""" + """Yields the next valid json object in a given string. + + Args: + text: Input string potentially containing one or more JSON objects. + + Yields: + Each valid JSON object found in ``text``, in order. + """ index = 0 decoder = json.JSONDecoder() @@ -317,7 +341,15 @@ def json_extraction(text: str) -> Generator[dict, None, None]: def find_func(d) -> tuple[str | None, Mapping | None]: """Find the first function in a json-like dictionary. - Most llms output tool requests in the form `...{"name": string, "arguments": {}}...` + Most llms output tool requests in the form ``...{"name": string, "arguments": {}}...`` + + Args: + d: A JSON-like Python object (typically a ``dict``) to search for a function + call record. + + Returns: + A ``(name, args)`` tuple where ``name`` is the tool name string and ``args`` + is the arguments mapping, or ``(None, None)`` if no function call was found. """ if not isinstance(d, dict): return None, None @@ -343,7 +375,14 @@ def find_func(d) -> tuple[str | None, Mapping | None]: def parse_tools(llm_response: str) -> list[tuple[str, Mapping]]: - """A simple parser that will scan a string for tools and attempt to extract them; only works for json based outputs.""" + """A simple parser that will scan a string for tools and attempt to extract them; only works for json based outputs. + + Args: + llm_response: Raw string output from a language model. + + Returns: + List of ``(tool_name, arguments)`` tuples for each tool call found. + """ processed = " ".join(llm_response.split()) tools = [] @@ -725,7 +764,18 @@ def _parse_docstring(doc_string: str | None) -> dict[str, str]: def convert_function_to_ollama_tool( func: Callable, name: str | None = None ) -> OllamaTool: - """Imported from Ollama.""" + """Convert a Python callable to an Ollama-compatible tool schema. + + Imported from Ollama. + + Args: + func: The Python callable to convert. + name: Optional override for the tool name; defaults to ``func.__name__``. + + Returns: + An ``OllamaTool`` instance representing the function as an OpenAI-compatible + tool schema. + """ doc_string_hash = str(hash(inspect.getdoc(func))) parsed_docstring = _parse_docstring(inspect.getdoc(func)) schema = type( diff --git a/mellea/backends/utils.py b/mellea/backends/utils.py index 472bbba4a..5044a0f2a 100644 --- a/mellea/backends/utils.py +++ b/mellea/backends/utils.py @@ -45,9 +45,18 @@ def to_chat( formatter: ChatFormatter, system_prompt: str | None, ) -> list[Chat]: - """Converts a context and an action into a series of dicts to be passed to apply_chat_template . + """Converts a context and an action into a series of dicts to be passed to apply_chat_template. This function is used by local inference backends. + + Args: + action: The next component or CBlock to generate output for. + ctx: The current conversation context. + formatter: The chat formatter used to convert context and action to messages. + system_prompt: Optional system prompt to prepend; overrides any system message in the context. + + Returns: + List of role/content dicts suitable for ``apply_chat_template``. """ assert ctx.is_chat_context @@ -82,7 +91,15 @@ def to_chat( def to_tool_calls( tools: dict[str, AbstractMelleaTool], decoded_result: str ) -> dict[str, ModelToolCall] | None: - """Parse a tool call string.""" + """Parse a tool call string. + + Args: + tools: Mapping of tool name to the corresponding ``AbstractMelleaTool`` object. + decoded_result: Raw model output string that may contain tool call markup. + + Returns: + Dict mapping tool name to validated ``ModelToolCall``, or ``None`` if no tool calls were found. + """ model_tool_calls: dict[str, ModelToolCall] = dict() for tool_name, tool_args in parse_tools(decoded_result): func = tools.get(tool_name) diff --git a/mellea/core/requirement.py b/mellea/core/requirement.py index bb58cd815..34f79d698 100644 --- a/mellea/core/requirement.py +++ b/mellea/core/requirement.py @@ -75,10 +75,16 @@ def __bool__(self) -> bool: def default_output_to_bool(x: CBlock | str) -> bool: - """Checks if a given output should be marked converted to `True`. + """Convert a model output string to a boolean by checking for a "yes" answer. - Checks if the output is exactly equal to "yes" or "y" (case-insensitive). If not, it will also - check if any of the words in the output are "yes" (case-insensitive). + Checks if the output is exactly equal to "yes" or "y" (case-insensitive). If not, + also checks if any of the words in the output are "yes" (case-insensitive). + + Args: + x: The model output to evaluate, as a ``CBlock`` or plain string. + + Returns: + ``True`` if the output indicates a "yes" answer, ``False`` otherwise. """ output = str(x) diff --git a/mellea/formatters/granite/base/optional.py b/mellea/formatters/granite/base/optional.py index 81c42018b..9bd6deaf2 100644 --- a/mellea/formatters/granite/base/optional.py +++ b/mellea/formatters/granite/base/optional.py @@ -23,7 +23,12 @@ @contextmanager def import_optional(extra_name: str): - """Handle optional imports.""" + """Handle optional imports. + + Args: + extra_name: Package extra to suggest in the install hint + (e.g. ``pip install granite_io[extra_name]``). + """ try: yield except ImportError as err: @@ -40,7 +45,8 @@ def import_optional(extra_name: str): def nltk_check(feature_name: str): """Variation on import_optional for nltk. - :param feature_name: Name of feature that requires NLTK + Args: + feature_name: Name of the feature that requires NLTK, used in the error message. """ try: yield diff --git a/mellea/formatters/granite/base/util.py b/mellea/formatters/granite/base/util.py index 1a11346d5..c47316d69 100644 --- a/mellea/formatters/granite/base/util.py +++ b/mellea/formatters/granite/base/util.py @@ -28,7 +28,12 @@ @contextlib.contextmanager def import_optional(extra_name: str): - """Handle optional imports.""" + """Handle optional imports. + + Args: + extra_name: Package extra to suggest in the install hint + (e.g. ``pip install granite_io[extra_name]``). + """ try: yield except ImportError as err: @@ -45,7 +50,8 @@ def import_optional(extra_name: str): def nltk_check(feature_name: str): """Variation on import_optional for nltk. - :param feature_name: Name of feature that requires NLTK + Args: + feature_name: Name of the feature that requires NLTK, used in the error message. """ try: yield @@ -62,6 +68,13 @@ def find_substring_in_text(substring: str, text: str) -> list[dict]: Given two strings - substring and text - find and return all matches of substring within text. For each match return its begin and end index. + + Args: + substring: The string to search for. + text: The string to search within. + + Returns: + List of dicts with ``begin_idx`` and ``end_idx`` for each match found. """ span_matches = [] @@ -73,7 +86,11 @@ def find_substring_in_text(substring: str, text: str) -> list[dict]: def random_uuid() -> str: - """:returns: hexadecimal data suitable to use as a unique identifier""" + """Generate a random UUID string. + + Returns: + Hexadecimal UUID string suitable for use as a unique identifier. + """ return str(uuid.uuid4()) @@ -84,9 +101,14 @@ def load_transformers_lora(local_or_remote_path): pass it a LoRA adapter's config, but that auto-loading is very broken as of 8/2025. Workaround powers activate! - Only works if ``transformers`` and ``peft`` are installed + Only works if ``transformers`` and ``peft`` are installed. - :returns: Tuple of LoRA model and tokenizer + Args: + local_or_remote_path: Local directory path of the LoRA adapter. + + Returns: + Tuple of ``(model, tokenizer)`` where ``model`` is the loaded LoRA model and + ``tokenizer`` is the corresponding HuggingFace tokenizer. """ with import_optional("peft"): # Third Party @@ -112,17 +134,18 @@ def chat_completion_request_to_transformers_inputs( Translate an OpenAI-style chat completion request into an input for a Transformers ``generate()`` call. - :param request: Request as parsed JSON or equivalent dataclass - :param tokenizer: Pointer to the HuggingFace tokenizer that will be used to handle - this request. Only required if the request uses constrained decoding. - :param model: Pointer to the HuggingFace model that will be used to handle - this request. Only required if the request uses constrained decoding. - :param constrained_decoding_prefix: Optional generation prefix to append to the - prompt - - :returns: Tuple of: - * kwargs to pass to generation - * Additional stuff to pass to generate_with_transformers + Args: + request: Request as parsed JSON or equivalent dataclass. + tokenizer: HuggingFace tokenizer for the model. Only required if the request + uses constrained decoding. + model: HuggingFace model object. Only required if the request uses constrained + decoding. + constrained_decoding_prefix: Optional generation prefix to append to the prompt. + + Returns: + Tuple of ``(generate_input, other_input)`` where ``generate_input`` contains + kwargs to pass directly to ``generate()`` and ``other_input`` contains + additional parameters for ``generate_with_transformers``. """ with import_optional("torch"): # Third Party @@ -285,15 +308,18 @@ def generate_with_transformers( There are quite a few extra steps. - :param tokenizer: Tokenizer for the model, required at several stages of generation - :param model: Initialized model object. - :param generate_input: Parameters to pass to the generate() method, usually - generated by :func:`chat_completion_request_to_transformers_inputs()` - :param other_input: Additional kwargs that - :func:`chat_completion_request_to_transformers_inputs()` added to encompass - aspects of the original request that Transformers APIs don't handle natively. - - :returns: A chat completion response in OpenAI format + Args: + tokenizer: HuggingFace tokenizer for the model, required at several stages + of generation. + model: Initialized HuggingFace model object. + generate_input: Parameters to pass to the ``generate()`` method, usually + produced by ``chat_completion_request_to_transformers_inputs()``. + other_input: Additional kwargs produced by + ``chat_completion_request_to_transformers_inputs()`` for aspects of the + original request that Transformers APIs don't handle natively. + + Returns: + A chat completion response in OpenAI format. """ with import_optional("torch"): # Third Party diff --git a/mellea/formatters/granite/granite3/output.py b/mellea/formatters/granite/granite3/output.py index 0d9c4d5a2..d3c31d96a 100644 --- a/mellea/formatters/granite/granite3/output.py +++ b/mellea/formatters/granite/granite3/output.py @@ -27,6 +27,15 @@ def create_dict(input_array, **key_attrib_names: str) -> dict: Given an array of dicts and the name of attribute(s) within the array, return a dict containing the contents of the array indexed by the given attribute(s) + + Args: + input_array: Iterable of dicts to index. + **key_attrib_names: Keyword arguments whose values are attribute names within + each dict. The resulting key is a hyphen-joined concatenation of those + attribute values. + + Returns: + Dict mapping the concatenated attribute value(s) to the original dict entry. """ new_dict: dict = {} @@ -67,6 +76,12 @@ def parse_hallucinations_text(hallucinations_text: str) -> list[dict]: }, ... ] + + Args: + hallucinations_text: Raw text from the model's "# Hallucinations:" section. + + Returns: + List of dicts, each with ``hallucination_id``, ``risk``, and ``response_text`` keys. """ hallucinations = [] @@ -160,6 +175,17 @@ def add_hallucination_response_spans( text (without citation tags)" "response_end": "The end index of "response_text" within the response text (without citation tags)" + + Args: + hallucination_info: Parsed hallucination list as returned by + ``parse_hallucinations_text``. + response_text_without_citations: Full response text with citation tags removed. + remove_citations_from_response_text: Callable that strips citation tags from + a substring of the response. + + Returns: + Deep copy of ``hallucination_info`` with ``response_text``, ``response_begin``, + and ``response_end`` populated for each entry. """ augmented_hallucination_info = copy.deepcopy(hallucination_info) @@ -214,6 +240,15 @@ def add_citation_context_spans( "context_begin": "The begin index of "context_text" within document with ID doc_id" "context_end": "The end index of "context_text" within document with ID doc_id" + + Args: + citation_info: List of citation dicts as produced by the model output parser. + docs: List of source document dicts, each with ``citation_id``, ``doc_id``, + and ``text`` keys. + + Returns: + Deep copy of ``citation_info`` with ``context_begin`` and ``context_end`` + populated for each entry. """ augmented_citation_info = copy.deepcopy(citation_info) docs_by_cit_doc_id = create_dict( diff --git a/mellea/formatters/granite/intrinsics/input.py b/mellea/formatters/granite/intrinsics/input.py index 7ce05708c..1f06454fa 100644 --- a/mellea/formatters/granite/intrinsics/input.py +++ b/mellea/formatters/granite/intrinsics/input.py @@ -36,11 +36,13 @@ def _needs_logprobs(transformations: list | None) -> bool: def sentence_delimiter(tag, sentence_num) -> str: """Return a tag string that identifies the beginning of the indicated sentence. - :param tag: tag string, i.e. "i" or "c" - :param sentence_num: index of sentence - :return: Tag string (including trailing space) that identifies the beginning of + Args: + tag: Tag string prefix, e.g. ``"i"`` or ``"c"``. + sentence_num: Zero-based index of the sentence. + + Returns: + Tag string (including trailing space) that identifies the beginning of the indicated sentence in sentence-tagged text. - :rtype: str """ return f"<{tag}{sentence_num}> " @@ -54,11 +56,13 @@ def mark_sentence_boundaries( ``<[prefix][number]>`` at the location of each sentence boundary. - :param split_strings: Input string(s), pre-split into sentences - :param tag_prefix: String to place before the number part of each tagged - sentence boundary. + Args: + split_strings: Input string(s), pre-split into sentences. + tag_prefix: String to place before the number part of each tagged + sentence boundary. - :returns: List of input strings with all sentence boundaries marked. + Returns: + List of input strings with all sentence boundaries marked. """ index = 0 result: list[str] = [] @@ -81,10 +85,13 @@ def move_documents_to_message( This function edits a request by putting the documents into the first turn of the messages. - :param chat_completion: A chat completion request as dataclass or parsed JSON - :param how: How to serialize the documents; supported values are "string" and "json" + Args: + chat_completion: A chat completion request as dataclass or parsed JSON. + how: How to serialize the documents; supported values are ``"string"``, + ``"json"``, and ``"roles"``. - :returns: A copy of ``chat_completion`` with any documents under ``extra_body`` + Returns: + A copy of ``chat_completion`` with any documents under ``extra_body`` moved to the first message. Returned type will be the same as the input type. May return original object if no edits are necessary. """ @@ -200,11 +207,12 @@ def __init__( ): """Initialize the IntrinsicsRewriter. - :param config_file: (optional) Location of YAML configuration file - :param config_dict: (optional) Contents of YAML configuration file that the - caller has parsed by calling ``yaml.safe_load()``. - :param model_name: (optional) model name to put into chat completion requests, - or ``None`` to use default model name from the configuration + Args: + config_file: Optional location of the YAML configuration file. + config_dict: Optional pre-parsed contents of the YAML configuration file + (result of ``yaml.safe_load()``). + model_name: Optional model name to inject into chat completion requests; + ``None`` uses the default from the configuration. """ config = make_config_dict(config_file, config_dict) if config is None: diff --git a/mellea/formatters/granite/intrinsics/json_util.py b/mellea/formatters/granite/intrinsics/json_util.py index 2868451f8..7e89d4e68 100644 --- a/mellea/formatters/granite/intrinsics/json_util.py +++ b/mellea/formatters/granite/intrinsics/json_util.py @@ -55,10 +55,12 @@ def find_string_offsets(json_data: str) -> list[tuple[int, int, str]]: Find the offsets of all strings in the input, assuming that this input contains valid JSON. - :param json_data: String containing valid JSON. + Args: + json_data: String containing valid JSON. - :returns: Begin and end offsets of all strings in ``json_data``, including - the double quotes. + Returns: + Begin and end offsets of all strings in ``json_data``, including + the double quotes. """ result = [] position = 0 @@ -75,12 +77,14 @@ def find_string_offsets(json_data: str) -> list[tuple[int, int, str]]: def non_string_offsets(json_str, compiled_regex, string_begins, string_ends): """Identify all matches of a regex that are not within string literals. - :param json_str: Original string of valid JSON data - :param compiled_regex: Compiled regex for the target token type - :param string_begins: table of string begin offsets within json_str - :param string_ends: table of string end offsets within json_str - :return: List of (begin, end, matched string) tuples - :rtype: list + Args: + json_str: Original string of valid JSON data. + compiled_regex: Compiled regex for the target token type. + string_begins: Table of string begin offsets within ``json_str``. + string_ends: Table of string end offsets within ``json_str``. + + Returns: + List of ``(begin, end, matched_string)`` tuples. """ offsets = [] for match in compiled_regex.finditer(json_str): @@ -100,9 +104,11 @@ def non_string_offsets(json_str, compiled_regex, string_begins, string_ends): def tokenize_json(json_str: str): """Lexer for parsing JSON. - :param json_str: String representation of valid JSON data. - :type json_str: str - :returns: List of tuples of (begin, end, value, type) + Args: + json_str: String representation of valid JSON data. + + Returns: + List of tuples of ``(begin, end, value, type)``. """ string_offsets = find_string_offsets(json_str) string_begins = [s[0] for s in string_offsets] @@ -134,9 +140,12 @@ def reparse_value(tokens, offset) -> tuple[Any, int]: Main entry point for a recursive-descent JSON parser with offset generation. Assumes valid JSON. - :param tokens: Token stream as produced by :func:`tokenize_json()` - :param offset: Token offset at which to start parsing - :return: Value parsed at the indicated offset in the token stream and next offset + Args: + tokens: Token stream as produced by ``tokenize_json()``. + offset: Token offset at which to start parsing. + + Returns: + Tuple of ``(parsed_value, next_offset)``. """ begin, end, value, type_ = tokens[offset] if type_ == "delim": @@ -214,12 +223,13 @@ def reparse_list(tokens, offset) -> tuple[list, int]: def reparse_json_with_offsets(json_str: str) -> Any: """Reparse a JSON string to compute the offsets of all literals. - :param json_str: String known to contain valid JSON data - :type json_str: str - :return: Parsed representation of ``json_str``, with literals at the leaf nodes of - the parse tree replaced with instances of :class:`JsonLiteralWithPosition` - containing position information. - :rtype: Any + Args: + json_str: String known to contain valid JSON data. + + Returns: + Parsed representation of ``json_str``, with literals at the leaf nodes of + the parse tree replaced with ``JsonLiteralWithPosition`` instances containing + position information. """ tokens = tokenize_json(json_str) return reparse_value(tokens, 0)[0] @@ -228,9 +238,11 @@ def reparse_json_with_offsets(json_str: str) -> Any: def scalar_paths(parsed_json) -> list[tuple]: """Get paths to all scalar values in parsed JSON. - :param parsed_json: JSON data parsed into native Python objects + Args: + parsed_json: JSON data parsed into native Python objects. - :returns: A list of paths to scalar values within ``parsed_json``, where each + Returns: + A list of paths to scalar values within ``parsed_json``, where each path is expressed as a tuple. The root element of a bare scalar is an empty tuple. """ @@ -250,9 +262,11 @@ def scalar_paths(parsed_json) -> list[tuple]: def all_paths(parsed_json) -> list[tuple]: """Find all possible paths within a parsed JSON value. - :param parsed_json: JSON data parsed into native Python objects + Args: + parsed_json: JSON data parsed into native Python objects. - :returns: A list of paths to elements of the parse tree of ``parsed_json``, + Returns: + A list of paths to all elements of the parse tree of ``parsed_json``, where each path is expressed as a tuple. The root element of a bare scalar is an empty tuple. """ @@ -269,11 +283,13 @@ def all_paths(parsed_json) -> list[tuple]: def fetch_path(json_value: Any, path: tuple): """Get the node at the indicated path in JSON. - :param json_value: Parsed JSON value - :param path: A tuple of names/numbers that indicates a path from root to a leaf - or internal node of ``json_value`` + Args: + json_value: Parsed JSON value. + path: A tuple of names/numbers that indicates a path from root to a leaf + or internal node of ``json_value``. - :returns: The node at the indicated leaf node + Returns: + The node at the indicated path. """ if not isinstance(path, tuple): raise TypeError(f"Expected tuple, but received '{type(path)}'") @@ -300,12 +316,13 @@ def fetch_path(json_value: Any, path: tuple): def replace_path(json_value: Any, path: tuple, new_value: Any) -> Any: """Modify a parsed JSON value in place by setting a particular path. - :param json_value: Parsed JSON value - :param path: A tuple of names/numbers that indicates a path from root to node of - ``json_value`` - :param new_value: New value to put in place at the indicated location + Args: + json_value: Parsed JSON value. + path: A tuple of names/numbers indicating a path from root to the target node. + new_value: New value to place at the indicated location. - :returns: The modified input, or a new value if the root was modified. + Returns: + The modified input, or ``new_value`` itself if the root was replaced. """ if not isinstance(path, tuple): raise TypeError(f"Expected tuple, but received '{type(path)}'") @@ -320,10 +337,12 @@ def replace_path(json_value: Any, path: tuple, new_value: Any) -> Any: def parse_inline_json(json_response: dict) -> dict: """Replace the JSON strings in message contents with parsed JSON. - :param json_response: parsed JSON representation of a ``ChatCompletionResponse`` - object. + Args: + json_response: Parsed JSON representation of a ``ChatCompletionResponse`` object. - :returns: Copy of the input with JSON message contents parsed. + Returns: + Deep copy of the input with JSON message content strings replaced by parsed + Python objects. """ result = copy.deepcopy(json_response) @@ -339,11 +358,13 @@ def parse_inline_json(json_response: dict) -> dict: def make_begin_to_token_table(logprobs: ChatCompletionLogProbs | None): """Create a table mapping token begin positions to token indices. - :param logprobs: The token log probabilities from the chat completion, or ``None`` - if the chat completion request did not ask for logprobs + Args: + logprobs: The token log probabilities from the chat completion, or ``None`` + if the chat completion request did not ask for logprobs. - :returns: A dictionary mapping token begin positions to token indices, - or ``None`` if logprobs is ``None``. + Returns: + A dictionary mapping token begin positions to token indices, + or ``None`` if ``logprobs`` is ``None``. """ if logprobs is None: return None diff --git a/mellea/formatters/granite/intrinsics/util.py b/mellea/formatters/granite/intrinsics/util.py index e20a16c21..0d10f7a84 100644 --- a/mellea/formatters/granite/intrinsics/util.py +++ b/mellea/formatters/granite/intrinsics/util.py @@ -29,8 +29,17 @@ def make_config_dict( This function is not a public API and is not intended for use outside this library. Common initialization code for reading YAML config files in factory classes. - Also parses JSON fields. + + Args: + config_file: Path to a YAML configuration file. Exactly one of ``config_file`` + and ``config_dict`` must be provided. + config_dict: Pre-parsed configuration dict (from ``yaml.safe_load()``). Exactly + one of ``config_file`` and ``config_dict`` must be provided. + + Returns: + Validated configuration dict with optional fields set to ``None`` and JSON + string fields parsed to Python objects. """ if (config_file is None and config_dict is None) or ( config_file is not None and config_dict is not None @@ -100,17 +109,20 @@ def obtain_lora( https://huggingface.co/ibm-granite/granite-lib-rag-r1.0). Caches the downloaded adapter files on local disk. - :param intrinsic_name: Short name of the intrinsic model, such as "certainty". - :param target_model_name: Name of the base model for the LoRA or aLoRA adapter. - :param repo_id: Optional name of Hugging Face Hub repository containing a collection - of LoRA and/or aLoRA adapters for intrinsics. - :param alora: If ``True``, load aLoRA version of intrinsic; otherwise use LoRA - :param cache_dir: Local directory to use as a cache (in Hugging Face Hub format), - or ``None`` to use the Hugging Face Hub default location. - :param file_glob: Only files that match this glob will be downloaded to the cache. - - :returns: the full path to the local copy of the specified (a)LoRA adapter. - This path is suitable for passing to commands that will serve the adapter. + Args: + intrinsic_name: Short name of the intrinsic model, such as ``"certainty"``. + target_model_name: Name of the base model for the LoRA or aLoRA adapter. + repo_id: Hugging Face Hub repository containing a collection of LoRA and/or + aLoRA adapters for intrinsics. + revision: Git revision of the repository to download from. + alora: If ``True``, load the aLoRA version of the intrinsic; otherwise use LoRA. + cache_dir: Local directory to use as a cache (Hugging Face Hub format), or + ``None`` to use the default location. + file_glob: Only files matching this glob will be downloaded to the cache. + + Returns: + Full path to the local copy of the specified (a)LoRA adapter, suitable for + passing to commands that serve the adapter. """ # Third Party import huggingface_hub @@ -168,17 +180,19 @@ def obtain_io_yaml( https://huggingface.co/ibm-granite/granite-lib-rag-r1.0) if one is not already in the local cache. - :param intrinsic_name: Short name of the intrinsic model, such as "certainty". - :param target_model_name: Name of the base model for the LoRA or aLoRA adapter. - :param repo_id: Optional name of Hugging Face Hub repository containing a collection - of LoRA and/or aLoRA adapters for intrinsics. - Default is to use rag-intrinsics-lib. - :param alora: If ``True``, load aLoRA version of intrinsic; otherwise use LoRA - :param cache_dir: Local directory to use as a cache (in Hugging Face Hub format), - or ``None`` to use the Hugging Face Hub default location. - - :returns: the full path to the local copy of the specified (a)LoRA adapter. - This path is suitable for passing to commands that will serve the adapter. + Args: + intrinsic_name: Short name of the intrinsic model, such as ``"certainty"``. + target_model_name: Name of the base model for the LoRA or aLoRA adapter. + repo_id: Hugging Face Hub repository containing a collection of LoRA and/or + aLoRA adapters for intrinsics. + revision: Git revision of the repository to download from. + alora: If ``True``, load the aLoRA version of the intrinsic; otherwise use LoRA. + cache_dir: Local directory to use as a cache (Hugging Face Hub format), or + ``None`` to use the default location. + + Returns: + Full path to the local copy of the ``io.yaml`` file, suitable for passing to + ``IntrinsicsRewriter``. """ lora_dir = obtain_lora( intrinsic_name, diff --git a/mellea/formatters/granite/retrievers/embeddings.py b/mellea/formatters/granite/retrievers/embeddings.py index b1811b41a..8081c3bf2 100644 --- a/mellea/formatters/granite/retrievers/embeddings.py +++ b/mellea/formatters/granite/retrievers/embeddings.py @@ -93,18 +93,20 @@ def compute_embeddings( # pylint: disable=too-many-locals """Split documents into windows and compute embeddings for each of the the windows. - :param corpus: PyArrow Table of documents as returned by :func:`read_corpus()`. - Should have the columns ``["id", "url", "title", "text"]`` - :param embedding_model_name: Hugging Face model name for the model that computes - embeddings. Also used for tokenizing. - :param chunk_size: Maximum size of chunks to split documents into, in embedding - model tokens; must be less than or equal to the embedding model's maximum - sequence length. - :param overlap: Target overlap between adjacent chunks, in embedding model tokens. - Actual begins and ends of chunks will be on sentence boundaries. - - :returns: PyArrow Table of chunks of the corpus, with schema - ````["id", "url", "title", "begin", "end", "text", "embedding"]`` + Args: + corpus: PyArrow Table of documents as returned by ``read_corpus()``. + Should have the columns ``["id", "url", "title", "text"]``. + embedding_model_name: Hugging Face model name for the model that computes + embeddings. Also used for tokenizing. + chunk_size: Maximum size of chunks to split documents into, in embedding + model tokens; must be less than or equal to the embedding model's maximum + sequence length. + overlap: Target overlap between adjacent chunks, in embedding model tokens. + Actual begins and ends of chunks will be on sentence boundaries. + + Returns: + PyArrow Table of chunks of the corpus, with schema + ``["id", "url", "title", "begin", "end", "text", "embedding"]``. """ # Third Party import pyarrow as pa @@ -187,12 +189,15 @@ def write_embeddings( Write the embeddings produced by :func:`compute_embeddings()` to a directory of Parquet files on local disk. - :param target_dir: Location where the files should be written (in a subdirectory) - :param corpus_name: Corpus name to use in generating a location for writing files - :param chunks_per_partition: Number of document chunks to write to each partition - of the Parquet file + Args: + target_dir: Location where the files should be written (in a subdirectory). + corpus_name: Corpus name used to generate the output directory name. + embeddings: PyArrow Table produced by ``compute_embeddings()``. + chunks_per_partition: Number of document chunks to write to each Parquet + partition file. - :param returns: Path where data was written + Returns: + Path to the directory where the Parquet files were written. """ # Third Party import pyarrow as pa @@ -221,12 +226,13 @@ def __init__( ): """Simple retriever that keeps docs and embeddings in memory. - :param data_file_or_table: Parquet file of document snippets and embeddings, - or an equivalent in-memory PyArrow Table object. - Should have columns `id`, `begin`, `end`, `text`, and `embedding`. - :param embedding_model_name: Name of Sentence Transformers model to use for - embeddings. Must be the same model that was used to compute embeddings in the - data file. + Args: + data_file_or_table: Parquet file of document snippets and embeddings, + or an equivalent in-memory PyArrow Table. Should have columns + ``id``, ``begin``, ``end``, ``text``, and ``embedding``. + embedding_model_name: Name of the Sentence Transformers model to use for + embeddings. Must match the model used to compute embeddings in the + data file. """ # Third Party import pyarrow as pa @@ -251,7 +257,15 @@ def __init__( self._embeddings = torch.tensor(embeddings_array) def retrieve(self, query: str, top_k: int = 5) -> list[dict]: - """Run a query and return results.""" + """Run a query and return results. + + Args: + query: Natural language query string. + top_k: Number of top results to return. + + Returns: + List of dicts with keys ``doc_id``, ``text``, and ``score``. + """ # Third Party import pyarrow as pa import sentence_transformers diff --git a/mellea/formatters/granite/retrievers/util.py b/mellea/formatters/granite/retrievers/util.py index 4da4dd041..5129dd3f8 100644 --- a/mellea/formatters/granite/retrievers/util.py +++ b/mellea/formatters/granite/retrievers/util.py @@ -17,10 +17,13 @@ def download_mtrag_corpus(target_dir: str, corpus_name: str) -> pathlib.Path: """Download a corpus file from the MTRAG benchmark if the file hasn't already present. - :param target_dir: Location where the file should be written if not already present. - :param corpus_name: Should be one of "cloud", "clapnq", "fiqa", or "govt" + Args: + target_dir: Location where the file should be written if not already present. + corpus_name: Should be one of ``"cloud"``, ``"clapnq"``, ``"fiqa"``, + or ``"govt"``. - :returns: Path to the downloaded (or cached) file. + Returns: + Path to the downloaded (or cached) file. """ corpus_names = ("cloud", "clapnq", "fiqa", "govt") if corpus_name not in corpus_names: @@ -42,10 +45,12 @@ def download_mtrag_corpus(target_dir: str, corpus_name: str) -> pathlib.Path: def read_mtrag_corpus(corpus_file: str | pathlib.Path) -> pa.Table: """Read the documents from one of the MTRAG benchmark's corpora. - :param corpus_file: Location where the corpus data is located. + Args: + corpus_file: Location of the corpus data file. - :returns: Documents from the corpus as a Pyarrow table, with schema - ``["id", "url", "title", "text"]`` + Returns: + Documents from the corpus as a PyArrow table, with schema + ``["id", "url", "title", "text"]``. """ if not isinstance(corpus_file, pathlib.Path): corpus_file = pathlib.Path(corpus_file) @@ -84,11 +89,13 @@ def read_mtrag_corpus(corpus_file: str | pathlib.Path) -> pa.Table: def download_mtrag_embeddings(embedding_name: str, corpus_name: str, target_dir: str): """Download precomputed embeddings for a corpus in the MTRAG benchmark. - :param embedding_name: Name of SentenceTransformers embedding model that was used - to create the embeddings. - :param corpus_name: Should be one of "cloud", "clapnq", "fiqa", or "govt" - :param target_dir: Location where Parquet files with names "part_001.parquet", - "part_002.parquet", etc. will be written. + Args: + embedding_name: Name of the SentenceTransformers embedding model used to + create the embeddings. + corpus_name: Should be one of ``"cloud"``, ``"clapnq"``, ``"fiqa"``, + or ``"govt"``. + target_dir: Location where Parquet files named ``"part_001.parquet"``, + ``"part_002.parquet"``, etc. will be written. """ corpus_names = ("cloud", "clapnq", "fiqa", "govt") if corpus_name not in corpus_names: diff --git a/mellea/helpers/async_helpers.py b/mellea/helpers/async_helpers.py index c3bfc86a0..b4935ee6f 100644 --- a/mellea/helpers/async_helpers.py +++ b/mellea/helpers/async_helpers.py @@ -19,7 +19,13 @@ async def send_to_queue( co: Coroutine[Any, Any, AsyncIterator | Any] | AsyncIterator, aqueue: asyncio.Queue ) -> None: - """Processes the output of an async chat request by sending the output to an async queue.""" + """Processes the output of an async chat request by sending the output to an async queue. + + Args: + co: A coroutine or async iterator producing the backend response. + aqueue: The async queue to send results to. A sentinel ``None`` is appended on + completion; an exception instance is appended on error. + """ try: if isinstance(co, Coroutine): aresponse = await co @@ -49,6 +55,9 @@ async def wait_for_all_mots(mots: list[ModelOutputThunk]) -> None: All ModelOutputThunks must be from the same event loop. This should always be the case in sampling functions, session functions, and top-level mellea functions. + + Args: + mots: List of ``ModelOutputThunk`` objects to await concurrently. """ coroutines: list[Coroutine[Any, Any, str]] = [] for mot in mots: @@ -58,7 +67,11 @@ async def wait_for_all_mots(mots: list[ModelOutputThunk]) -> None: def get_current_event_loop() -> None | asyncio.AbstractEventLoop: - """Get the current event loop without having to catch exceptions.""" + """Get the current event loop without having to catch exceptions. + + Returns: + The running event loop, or ``None`` if no loop is running. + """ loop = None try: loop = asyncio.get_running_loop() @@ -77,16 +90,30 @@ def __init__(self, capacity: int): """Initializes the LRU cache with a certain capacity. The `ClientCache` either contains a value or it doesn't. + + Args: + capacity: Maximum number of entries to hold before evicting the least recently used. """ self.capacity = capacity self.cache: OrderedDict = OrderedDict() def current_size(self) -> int: - """Just return the size of the key set. This isn't necessarily safe.""" + """Just return the size of the key set. This isn't necessarily safe. + + Returns: + Number of entries currently in the cache. + """ return len(self.cache.keys()) def get(self, key: int) -> Any | None: - """Gets a value from the cache.""" + """Gets a value from the cache. + + Args: + key: Integer cache key. + + Returns: + The cached value, or ``None`` if the key is not present. + """ if key not in self.cache: return None else: @@ -96,7 +123,12 @@ def get(self, key: int) -> Any | None: return value def put(self, key: int, value: Any) -> None: - """Put a value into the cache.""" + """Put a value into the cache. + + Args: + key: Integer cache key. + value: Value to store. + """ if key in self.cache: # If the key exists, move it to the end (most recent) self.cache.pop(key) diff --git a/mellea/helpers/openai_compatible_helpers.py b/mellea/helpers/openai_compatible_helpers.py index 5afec1232..783ce8c7f 100644 --- a/mellea/helpers/openai_compatible_helpers.py +++ b/mellea/helpers/openai_compatible_helpers.py @@ -13,7 +13,17 @@ def extract_model_tool_requests( tools: dict[str, AbstractMelleaTool], response: dict[str, Any] ) -> dict[str, ModelToolCall] | None: - """Extracts tool calls from the dict representation of an OpenAI-like chat response object.""" + """Extract tool calls from the dict representation of an OpenAI-like chat response object. + + Args: + tools: Mapping of tool name to ``AbstractMelleaTool`` for lookup. + response: Dict representation of an OpenAI-compatible chat completion message + (must contain a ``"message"`` key). + + Returns: + Mapping of tool name to ``ModelToolCall`` for each requested tool call, or + ``None`` if no tool calls were found. + """ model_tool_calls: dict[str, ModelToolCall] = {} calls = response["message"].get("tool_calls", None) if calls: @@ -45,11 +55,19 @@ def extract_model_tool_requests( def chat_completion_delta_merge( chunks: list[dict], force_all_tool_calls_separate: bool = False ) -> dict: - """Takes a list of deltas from `ChatCompletionChunk`s and merges them into a single dict representing the `ChatCompletion` choice. + """Merge a list of deltas from ``ChatCompletionChunk``s into a single dict representing the ``ChatCompletion`` choice. Args: - chunks: the list of dicts that represent the message deltas - force_all_tool_calls_separate: if `True`, tool calls in separate message deltas will not be merged (even if their index values are the same); use when providers do not return the correct index value for tool calls. If using this option, all tool calls must be fully populated in a single delta since they won't be merged. + chunks: The list of dicts that represent the message deltas. + force_all_tool_calls_separate: If ``True``, tool calls in separate message + deltas will not be merged even if their index values are the same. Use + when providers do not return the correct index value for tool calls; all + tool calls must then be fully populated in a single delta. + + Returns: + A single merged dict representing the assembled ``ChatCompletion`` choice, + with ``finish_reason``, ``index``, and a ``message`` sub-dict containing + ``content``, ``role``, and ``tool_calls``. """ merged: dict[str, Any] = dict() diff --git a/mellea/helpers/server_type.py b/mellea/helpers/server_type.py index 93c60a0c0..b3a770935 100644 --- a/mellea/helpers/server_type.py +++ b/mellea/helpers/server_type.py @@ -53,8 +53,11 @@ def is_vllm_server_with_structured_output( v0.12.0 was the last version to support guided_json params. It's now under structured_outputs. Args: - base_url : Base url for LLM API. - headers : additional headers to pass to the request. + base_url: Base url for LLM API. + headers: Additional headers to pass to the request. + + Returns: + True if the server is vLLM >= v0.12.0, False otherwise. """ # Not using the models endpoint for now. Assuming version is enough. # vllm_provider_endpoint = str(self._client.base_url).join("/models") diff --git a/mellea/stdlib/components/chat.py b/mellea/stdlib/components/chat.py index 3f199f3f0..ce1fcfdc6 100644 --- a/mellea/stdlib/components/chat.py +++ b/mellea/stdlib/components/chat.py @@ -214,7 +214,15 @@ def __repr__(self) -> str: def as_chat_history(ctx: Context) -> list[Message]: - """Returns a list of Messages corresponding to a Context.""" + """Returns a list of Messages corresponding to a Context. + + Args: + ctx: A linear ``Context`` whose entries are ``Message`` or ``ModelOutputThunk`` + objects with ``Message`` parsed representations. + + Returns: + List of ``Message`` objects in conversation order. + """ def _to_msg(c: CBlock | Component | ModelOutputThunk) -> Message | None: match c: diff --git a/mellea/stdlib/components/intrinsic/rag.py b/mellea/stdlib/components/intrinsic/rag.py index 5289b8d88..cf14d24b1 100644 --- a/mellea/stdlib/components/intrinsic/rag.py +++ b/mellea/stdlib/components/intrinsic/rag.py @@ -89,15 +89,17 @@ def check_answerability( Intrinsic function that checks whether the question in the last user turn of a chat can be answered by a provided set of RAG documents. - :param context: Chat context containing the conversation thus far - :param question: Question that the user has posed in response to the last turn in - ``context``. - :param documents: Document snippets retrieved that may or may not answer the - indicated question. - :param backend: Backend instance that supports adding the LoRA or aLoRA adapters - for answerability checks - - :return: Answerability score as a floating-point value from 0 to 1. + Args: + question: Question that the user has posed in response to the last turn in + ``context``. + documents: Document snippets retrieved that may or may not answer the + indicated question. + context: Chat context containing the conversation thus far. + backend: Backend instance that supports adding the LoRA or aLoRA adapters + for answerability checks. + + Returns: + Answerability score as a floating-point value from 0 to 1. """ result_json = _call_intrinsic( "answerability", @@ -115,12 +117,14 @@ def rewrite_question( Intrinsic function that rewrites the question in the next user turn into a self-contained query that can be passed to the retriever. - :param context: Chat context containing the conversation thus far - :param question: Question that the user has posed in response to the last turn in - ``context``. - :param backend: Backend instance that supports adding the LoRA or aLoRA adapters + Args: + question: Question that the user has posed in response to the last turn in + ``context``. + context: Chat context containing the conversation thus far. + backend: Backend instance that supports adding the LoRA or aLoRA adapters. - :return: Rewritten version of ``question``. + Returns: + Rewritten version of ``question``. """ result_json = _call_intrinsic( "query_rewrite", context.add(Message("user", question)), backend @@ -140,14 +144,16 @@ def clarify_query( based on the retrieved documents and conversation context, and generates an appropriate clarification question if needed. - :param context: Chat context containing the conversation thus far - :param question: Question that the user has posed - :param documents: Document snippets retrieved for the question - :param backend: Backend instance that supports the adapters that implement - this intrinsic + Args: + question: Question that the user has posed. + documents: Document snippets retrieved for the question. + context: Chat context containing the conversation thus far. + backend: Backend instance that supports the adapters that implement + this intrinsic. - :return: Clarification question string (e.g., "Do you mean A or B?"), or - the string "CLEAR" if no clarification is needed + Returns: + Clarification question string (e.g., "Do you mean A or B?"), or + the string "CLEAR" if no clarification is needed. """ result_json = _call_intrinsic( "query_clarification", @@ -168,23 +174,21 @@ def find_citations( Intrinsic function that finds sentences in RAG documents that support sentences in a potential assistant response to a user question. - :param context: Context of the dialog between user and assistant at the point where - the user has just asked a question that will be answered with RAG documents - :param response: Potential assistant response - :param documents: Documents at were used to generate ``response``. These documents - should set the ``doc_id`` field; otherwise the intrinsic will be unable to - specify which document was the source of a given citation. - :param backend: Backend that supports one of the adapters that implements this - intrinsic. - :return: List of records with the following fields: - * ``response_begin`` - * ``response_end`` - * ``response_text`` - * ``citation_doc_id`` - * ``citation_begin`` - * ``citation_end`` - * ``citation_text`` - Begin and end offsets are character offsets into their respective UTF-8 strings. + Args: + response: Potential assistant response. + documents: Documents that were used to generate ``response``. These documents + should set the ``doc_id`` field; otherwise the intrinsic will be unable to + specify which document was the source of a given citation. + context: Context of the dialog between user and assistant at the point where + the user has just asked a question that will be answered with RAG documents. + backend: Backend that supports one of the adapters that implements this + intrinsic. + + Returns: + List of records with the following fields: ``response_begin``, + ``response_end``, ``response_text``, ``citation_doc_id``, ``citation_begin``, + ``citation_end``, ``citation_text``. Begin and end offsets are character + offsets into their respective UTF-8 strings. """ result_json = _call_intrinsic( "citations", @@ -203,13 +207,15 @@ def check_context_relevance( the answer to a user's question. Does not consider the context in which the question was asked. - :param context: The chat up to the point where the user asked a question. - :param question: Question that the user has posed. - :param document: A retrieved document snippet - :param backend: Backend instance that supports the adapters that implement this - intrinsic + Args: + question: Question that the user has posed. + document: A retrieved document snippet. + context: The chat up to the point where the user asked a question. + backend: Backend instance that supports the adapters that implement this + intrinsic. - :return: Context relevance score as a floating-point value from 0 to 1. + Returns: + Context relevance score as a floating-point value from 0 to 1. """ result_json = _call_intrinsic( "context_relevance", @@ -233,19 +239,18 @@ def flag_hallucinated_content( user question are faithful to the retrieved document snippets. Sentences that do not align with the retrieved snippets are flagged as potential hallucinations. - :param context: A chat log that ends with a user asking a question - :param response: The assistant's response to the user's question in the last turn - of ``context`` - :param documents: Document snippets that were used to generate ``response`` - :param backend: Backend instance that supports the adapters that implement this - intrinsic - - :return: List of records with the following fields: - * response_begin - * response_end - * response_text - * faithfulness_likelihood - * explanation + Args: + response: The assistant's response to the user's question in the last turn + of ``context``. + documents: Document snippets that were used to generate ``response``. + context: A chat log that ends with a user asking a question. + backend: Backend instance that supports the adapters that implement this + intrinsic. + + Returns: + List of records with the following fields: ``response_begin``, + ``response_end``, ``response_text``, ``faithfulness_likelihood``, + ``explanation``. """ result_json = _call_intrinsic( "hallucination_detection", @@ -265,18 +270,19 @@ def rewrite_answer_for_relevance( ) -> str: """Rewrite an assistant answer to improve relevance to the user's question. - :param context: A chat log that ends with a user asking a question - :param response: The assistant's response to the user's question in the last turn - of ``context`` - :param documents: Document snippets that were used to generate ``response`` - :param backend: Backend instance that supports the adapters that implement this - intrinsic - :param rewrite_threshold: Number between 0.0 and 1.0 that determines how eagerly - to skip rewriting the assistant's answer for relevance. 0.0 means never rewrite - and 1.0 means always rewrite. - - :returns: Either the original response, or a rewritten version of the original - response. + Args: + response: The assistant's response to the user's question in the last turn + of ``context``. + documents: Document snippets that were used to generate ``response``. + context: A chat log that ends with a user asking a question. + backend: Backend instance that supports the adapters that implement this + intrinsic. + rewrite_threshold: Number between 0.0 and 1.0 that determines how eagerly + to skip rewriting the assistant's answer for relevance. 0.0 means never + rewrite and 1.0 means always rewrite. + + Returns: + Either the original response, or a rewritten version of the original response. """ # First run the classifier to determine the likelihood of a relevant answer # Output will have three fields: diff --git a/mellea/stdlib/functional.py b/mellea/stdlib/functional.py index 41aa1fbf7..a9caab140 100644 --- a/mellea/stdlib/functional.py +++ b/mellea/stdlib/functional.py @@ -239,7 +239,22 @@ def chat( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[Message, Context]: - """Sends a simple chat message and returns the response. Adds both messages to the Context.""" + """Sends a simple chat message and returns the response. Adds both messages to the Context. + + Args: + content: The message text to send. + context: The current conversation context. + backend: The backend used to generate the response. + role: The role for the outgoing message (default ``"user"``). + images: Optional list of images to include in the message. + user_variables: Optional Jinja variable substitutions applied to ``content``. + format: Optional Pydantic model for constrained decoding of the response. + model_options: Additional model options to merge with backend defaults. + tool_calls: If true, tool calling is enabled. + + Returns: + Tuple of the assistant ``Message`` and the updated ``Context``. + """ if user_variables is not None: content_resolved = Instruction.apply_user_dict_from_jinja( user_variables, content @@ -276,7 +291,21 @@ def validate( | None = None, # TODO: Can we get rid of gen logs here and in act? input: CBlock | None = None, ) -> list[ValidationResult]: - """Validates a set of requirements over the output (if provided) or the current context (if the output is not provided).""" + """Validates a set of requirements over the output (if provided) or the current context (if the output is not provided). + + Args: + reqs: A single ``Requirement`` or a list of them to validate. + context: The current conversation context. + backend: The backend used for LLM-as-a-judge requirements. + output: Optional model output ``CBlock`` to validate against instead of the context. + format: Optional Pydantic model for constrained decoding. + model_options: Additional model options to merge with backend defaults. + generate_logs: Optional list to append generation logs to. + input: Optional input ``CBlock`` to include alongside ``output`` when validating. + + Returns: + List of ``ValidationResult`` objects, one per requirement. + """ # Run everything in the specific event loop for this session. out = _run_async_in_thread( @@ -784,7 +813,22 @@ async def achat( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[Message, Context]: - """Sends a simple chat message and returns the response. Adds both messages to the Context.""" + """Sends a simple chat message and returns the response. Adds both messages to the Context. + + Args: + content: The message text to send. + context: The current conversation context. + backend: The backend used to generate the response. + role: The role for the outgoing message (default ``"user"``). + images: Optional list of images to include in the message. + user_variables: Optional Jinja variable substitutions applied to ``content``. + format: Optional Pydantic model for constrained decoding of the response. + model_options: Additional model options to merge with backend defaults. + tool_calls: If true, tool calling is enabled. + + Returns: + Tuple of the assistant ``Message`` and the updated ``Context``. + """ if user_variables is not None: content_resolved = Instruction.apply_user_dict_from_jinja( user_variables, content @@ -820,7 +864,21 @@ async def avalidate( generate_logs: list[GenerateLog] | None = None, input: CBlock | None = None, ) -> list[ValidationResult]: - """Asynchronous version of .validate; validates a set of requirements over the output (if provided) or the current context (if the output is not provided).""" + """Asynchronous version of .validate; validates a set of requirements over the output (if provided) or the current context (if the output is not provided). + + Args: + reqs: A single ``Requirement`` or a list of them to validate. + context: The current conversation context. + backend: The backend used for LLM-as-a-judge requirements. + output: Optional model output ``CBlock`` to validate against instead of the context. + format: Optional Pydantic model for constrained decoding. + model_options: Additional model options to merge with backend defaults. + generate_logs: Optional list to append generation logs to. + input: Optional input ``CBlock`` to include alongside ``output`` when validating. + + Returns: + List of ``ValidationResult`` objects, one per requirement. + """ # Turn a solitary requirement in to a list of requirements, and then reqify if needed. reqs = [reqs] if not isinstance(reqs, list) else reqs reqs = [Requirement(req) if type(req) is str else req for req in reqs] diff --git a/mellea/stdlib/requirements/md.py b/mellea/stdlib/requirements/md.py index e4f8bca34..5d21223c3 100644 --- a/mellea/stdlib/requirements/md.py +++ b/mellea/stdlib/requirements/md.py @@ -25,7 +25,15 @@ def _get_mistletoe() -> Any: def as_markdown_list(ctx: Context) -> list[str] | None: - """Attempts to format the last_output of the given context as a markdown list.""" + """Attempts to format the last_output of the given context as a markdown list. + + Args: + ctx: The current conversation context whose last output will be parsed. + + Returns: + List of rendered list-item strings if the output is a markdown list, + or ``None`` if parsing fails or the output is not a list. + """ mistletoe = _get_mistletoe() xs = list() raw_output = ctx.last_output() diff --git a/mellea/stdlib/requirements/requirement.py b/mellea/stdlib/requirements/requirement.py index 17799eb5e..09e7a1a60 100644 --- a/mellea/stdlib/requirements/requirement.py +++ b/mellea/stdlib/requirements/requirement.py @@ -15,10 +15,17 @@ class LLMaJRequirement(Requirement): def requirement_check_to_bool(x: CBlock | str) -> bool: - """Checks if a given output should be marked converted to `True`. + """Checks if a given output should be marked converted to ``True``. - By default, the requirement check alora outputs: `{"requirement_likelihood": 0.0}`. - True if >.5 + By default, the requirement check alora outputs: ``{"requirement_likelihood": 0.0}``. + Returns ``True`` if the likelihood value is > 0.5. + + Args: + x: ALoRA output string or CBlock containing JSON with a + ``requirement_likelihood`` field. + + Returns: + True if the extracted likelihood exceeds 0.5, False otherwise. """ output = str(x) req_dict: dict[str, Any] = json.loads(output) @@ -67,6 +74,12 @@ def reqify(r: str | Requirement) -> Requirement: """Maps strings to Requirements. This is a utility method for functions that allow you to pass in Requirements as either explicit Requirement objects or strings that you intend to be interpreted as requirements. + + Args: + r: A ``Requirement`` object or a plain string description to wrap as one. + + Returns: + A ``Requirement`` instance. """ if type(r) is str: return Requirement(r) @@ -77,12 +90,20 @@ def reqify(r: str | Requirement) -> Requirement: def req(*args, **kwargs) -> Requirement: - """Shorthand for Requirement.__init__.""" + """Shorthand for Requirement.__init__. + + Returns: + A new ``Requirement`` instance. + """ return Requirement(*args, **kwargs) def check(*args, **kwargs) -> Requirement: - """Shorthand for Requirement.__init__(..., check_only=True).""" + """Shorthand for Requirement.__init__(..., check_only=True). + + Returns: + A new ``Requirement`` instance with ``check_only=True``. + """ return Requirement(*args, **kwargs, check_only=True) @@ -116,6 +137,9 @@ def simple_validate( Args: fn: the simple validation function that takes a string and returns either a bool or (bool, str) reason: only used if the provided function returns a bool; if the validation function fails, a static reason for that failure to give to the llm when repairing + + Returns: + A validation function that takes a ``Context`` and returns a ``ValidationResult``. """ def validate(ctx: Context) -> ValidationResult: diff --git a/mellea/stdlib/requirements/tool_reqs.py b/mellea/stdlib/requirements/tool_reqs.py index 13471ae03..007b60114 100644 --- a/mellea/stdlib/requirements/tool_reqs.py +++ b/mellea/stdlib/requirements/tool_reqs.py @@ -30,7 +30,10 @@ def uses_tool(tool_name: str | Callable, check_only: bool = False) -> Requiremen tool_name: The tool that must be called; this can be either the name of the tool or the Callable for the tool. check_only: Propagates to the Requirement. - Use `tool_choice` if the OpenAI `tool_choice` model option is supported by your model and inference engine. + Use ``tool_choice`` if the OpenAI ``tool_choice`` model option is supported by your model and inference engine. + + Returns: + A ``Requirement`` that validates whether the specified tool was called. """ tool_name = _name2str(tool_name) @@ -69,6 +72,9 @@ def tool_arg_validator( Todo: 1. should this be a requirement? 2. should this be done automatically when the user provides asserts in their function body? + + Returns: + A ``Requirement`` that validates the specified tool argument. """ if tool_name: tool_name = _name2str(tool_name) diff --git a/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py b/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py index 86e925486..b3fc63db9 100644 --- a/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py +++ b/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py @@ -22,7 +22,7 @@ ) -async def think_budget_forcing( # noqa: D417 +async def think_budget_forcing( backend: OllamaModelBackend, action: CBlock | Component, *, @@ -46,17 +46,28 @@ async def think_budget_forcing( # noqa: D417 This is performed via multi-step generation. The model will be called multiple times until requirements are met, in other words, the response will be assembled conditionally. Args: - backend: OllamaModelBackend - action: The last item of the context should be passed in as an `action` instead of as part of the `ctx`. See `docs/dev/generate_signature_decisions.md`. - think_max_tokens: Budget in number of tokens allocated for the think block - answer_max_tokens: Budget in number of tokens allocated for the summary and answer block, None indicates unbounded answer, generating till EoS - start_think_token: String indicating start of think block, default - end_think_token: String indicating end of think block, default - begin_response_token: Used by certain models, string indicating start of response block, e.g. "", default None - think_more_suffix: String to append to force continued thinking, e.g. "\nWait" if set to None we will not force additional thinking. Use None for upper-bound budget case - answer_suffix: String to append to force a final answer + backend: OllamaModelBackend instance to use for generation. + action: The last item of the context, passed as an ``action`` instead of as part + of the ``ctx``. See ``docs/dev/generate_signature_decisions.md``. + ctx: The current conversation context. + format: Optional Pydantic model for constrained decoding of the response. + tool_calls: If ``True``, tool calling is enabled. + think_max_tokens: Budget in number of tokens allocated for the think block. + answer_max_tokens: Budget in number of tokens allocated for the summary and + answer block; ``None`` indicates unbounded answer, generating till EoS. + start_think_token: String indicating start of think block, default ````. + end_think_token: String indicating end of think block, default ````. + begin_response_token: Used by certain models, string indicating start of + response block, e.g. ``""``, default ``""``. + think_more_suffix: String to append to force continued thinking, e.g. + ``"\nWait"``; if ``None``, additional thinking is not forced (upper-bound + budget case). + answer_suffix: String to append to force a final answer. model_options: Any model options to upsert into the defaults for this call. + Returns: + A ``ModelOutputThunk`` containing the assembled thinking and answer response. + Assumptions: - The chat template is applied on prompt, with think mode enabled - Model is think mode activated diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 04d8c112e..c9f41b5ce 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -52,6 +52,9 @@ def get_session() -> MelleaSession: """Get the current session from context. + Returns: + The currently active ``MelleaSession``. + Raises: RuntimeError: If no session is currently active. """ @@ -64,7 +67,15 @@ def get_session() -> MelleaSession: def backend_name_to_class(name: str) -> Any: - """Resolves backend names to Backend classes.""" + """Resolves backend names to Backend classes. + + Args: + name: Short backend name, e.g. ``"ollama"``, ``"hf"``, ``"openai"``, + ``"watsonx"``, or ``"litellm"``. + + Returns: + The corresponding ``Backend`` class, or ``None`` if the name is unrecognised. + """ if name == "ollama": from ..backends.ollama import OllamaModelBackend @@ -511,6 +522,10 @@ def instruct( model_options: Additional model options, which will upsert into the model/backend's defaults. tool_calls: If true, tool calling is enabled. images: A list of images to be used in the instruction or None if none. + + Returns: + A ``ModelOutputThunk`` if ``return_sampling_results`` is ``False``, + else a ``SamplingResult``. """ r = mfuncs.instruct( description, @@ -550,7 +565,20 @@ def chat( model_options: dict | None = None, tool_calls: bool = False, ) -> Message: - """Sends a simple chat message and returns the response. Adds both messages to the Context.""" + """Sends a simple chat message and returns the response. Adds both messages to the Context. + + Args: + content: The message text to send. + role: The role for the outgoing message (default ``"user"``). + images: Optional list of images to include in the message. + user_variables: Optional Jinja variable substitutions applied to ``content``. + format: Optional Pydantic model for constrained decoding of the response. + model_options: Additional model options to merge with backend defaults. + tool_calls: If true, tool calling is enabled. + + Returns: + The assistant ``Message`` response. + """ result, context = mfuncs.chat( content=content, context=self.ctx, @@ -576,7 +604,19 @@ def validate( generate_logs: list[GenerateLog] | None = None, input: CBlock | None = None, ) -> list[ValidationResult]: - """Validates a set of requirements over the output (if provided) or the current context (if the output is not provided).""" + """Validates a set of requirements over the output (if provided) or the current context (if the output is not provided). + + Args: + reqs: A single ``Requirement`` or a list of them to validate. + output: Optional model output ``CBlock`` to validate against instead of the context. + format: Optional Pydantic model for constrained decoding. + model_options: Additional model options to merge with backend defaults. + generate_logs: Optional list to append generation logs to. + input: Optional input ``CBlock`` to include alongside ``output`` when validating. + + Returns: + List of ``ValidationResult`` objects, one per requirement. + """ return mfuncs.validate( reqs=reqs, context=self.ctx, @@ -795,6 +835,10 @@ async def ainstruct( model_options: Additional model options, which will upsert into the model/backend's defaults. tool_calls: If true, tool calling is enabled. images: A list of images to be used in the instruction or None if none. + + Returns: + A ``ModelOutputThunk`` if ``return_sampling_results`` is ``False``, + else a ``SamplingResult``. """ r = await mfuncs.ainstruct( description, @@ -834,7 +878,20 @@ async def achat( model_options: dict | None = None, tool_calls: bool = False, ) -> Message: - """Sends a simple chat message and returns the response. Adds both messages to the Context.""" + """Sends a simple chat message and returns the response. Adds both messages to the Context. + + Args: + content: The message text to send. + role: The role for the outgoing message (default ``"user"``). + images: Optional list of images to include in the message. + user_variables: Optional Jinja variable substitutions applied to ``content``. + format: Optional Pydantic model for constrained decoding of the response. + model_options: Additional model options to merge with backend defaults. + tool_calls: If true, tool calling is enabled. + + Returns: + The assistant ``Message`` response. + """ result, context = await mfuncs.achat( content=content, context=self.ctx, @@ -860,7 +917,19 @@ async def avalidate( generate_logs: list[GenerateLog] | None = None, input: CBlock | None = None, ) -> list[ValidationResult]: - """Validates a set of requirements over the output (if provided) or the current context (if the output is not provided).""" + """Validates a set of requirements over the output (if provided) or the current context (if the output is not provided). + + Args: + reqs: A single ``Requirement`` or a list of them to validate. + output: Optional model output ``CBlock`` to validate against instead of the context. + format: Optional Pydantic model for constrained decoding. + model_options: Additional model options to merge with backend defaults. + generate_logs: Optional list to append generation logs to. + input: Optional input ``CBlock`` to include alongside ``output`` when validating. + + Returns: + List of ``ValidationResult`` objects, one per requirement. + """ return await mfuncs.avalidate( reqs=reqs, context=self.ctx, diff --git a/mellea/stdlib/tools/interpreter.py b/mellea/stdlib/tools/interpreter.py index 166ef0b39..14aee139f 100644 --- a/mellea/stdlib/tools/interpreter.py +++ b/mellea/stdlib/tools/interpreter.py @@ -277,6 +277,9 @@ def code_interpreter(code: str) -> ExecutionResult: Args: code: The Python code to execute. + + Returns: + An ``ExecutionResult`` with stdout, stderr, and a success flag. """ exec_env = LLMSandboxEnvironment(allowed_imports=None) return exec_env.execute(code, 60) @@ -287,6 +290,9 @@ def local_code_interpreter(code: str) -> ExecutionResult: Args: code: The Python code to execute. + + Returns: + An ``ExecutionResult`` with stdout, stderr, and a success flag. """ exec_env = UnsafeEnvironment(allowed_imports=None) return exec_env.execute(code, 60) diff --git a/mellea/telemetry/tracing.py b/mellea/telemetry/tracing.py index 605d2d20a..cab2c2bc6 100644 --- a/mellea/telemetry/tracing.py +++ b/mellea/telemetry/tracing.py @@ -92,12 +92,22 @@ def _setup_tracer_provider() -> Any: def is_application_tracing_enabled() -> bool: - """Check if application tracing is enabled.""" + """Check if application tracing is enabled. + + Returns: + True if application tracing has been enabled via the + ``MELLEA_TRACE_APPLICATION`` environment variable. + """ return _TRACE_APPLICATION_ENABLED def is_backend_tracing_enabled() -> bool: - """Check if backend tracing is enabled.""" + """Check if backend tracing is enabled. + + Returns: + True if backend tracing has been enabled via the + ``MELLEA_TRACE_BACKEND`` environment variable. + """ return _TRACE_BACKEND_ENABLED From 765e7eeb85fcee2c8282879a48edbbc53dff20ee Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Wed, 11 Mar 2026 08:08:24 +0000 Subject: [PATCH 04/14] docs: expand docstrings for public classes and functions (#613) Add or expand Google-style docstrings across CLI and library modules: - cli/alora/train.py: add docstrings to load_dataset_from_json, formatting_prompts_func, SaveBestModelCallback, SafeSaveTrainer, train_model - cli/decompose/decompose.py: add class docstring to DecompVersion, expand run() with full Args section - cli/decompose/pipeline.py: add class docstrings to ConstraintResult, DecompSubtasksResult, DecompPipelineResult, DecompBackend; add full docstring with Args/Returns to decompose() - cli/decompose/utils.py: add Args/Returns to validate_filename - cli/eval/commands.py: add full docstring with Args to eval_run - cli/m.py: expand callback docstring - mellea/backends/adapters/adapter.py: expand LocalHFAdapter docstring - mellea/backends/cache.py: expand SimpleLRUCache docstring - mellea/backends/tools.py: expand SubscriptableBaseModel docstring; fix json_extraction to use Returns: instead of Yields: - mellea/core/backend.py: expand Backend docstring; add Args/Returns to generate_walk - mellea/core/base.py: add Args/Returns to blockify and get_images_from_component - mellea/core/utils.py: expand RESTHandler, JsonFormatter, FancyLogger class docstrings - mellea/helpers/openai_compatible_helpers.py: add Args/Returns to message_to_openai_message and messages_to_docs - mellea/stdlib/components/docs/richdocument.py: expand TableQuery and TableTransform class docstrings - mellea/stdlib/components/genslot.py: expand Argument, Function, Arguments, GenerativeSlot class docstrings; add SyncGenerativeSlot class docstring - mellea/stdlib/components/mobject.py: expand Query and Transform class docstrings - mellea/stdlib/requirements/requirement.py: add Args sections to req() and check() - mellea/stdlib/sampling/budget_forcing.py: expand BudgetForcingSamplingStrategy class docstring --- TASK.md | 67 +++++++++++++++++++ cli/alora/train.py | 52 ++++++++++++++ cli/decompose/decompose.py | 33 ++++++++- cli/decompose/pipeline.py | 32 +++++++++ cli/decompose/utils.py | 12 ++++ cli/eval/commands.py | 19 ++++++ cli/m.py | 7 +- mellea/backends/adapters/adapter.py | 6 +- mellea/backends/cache.py | 7 +- mellea/backends/tools.py | 14 ++-- mellea/core/backend.py | 18 ++++- mellea/core/base.py | 19 +++++- mellea/core/utils.py | 21 +++++- mellea/helpers/openai_compatible_helpers.py | 21 +++++- mellea/stdlib/components/docs/richdocument.py | 12 +++- mellea/stdlib/components/genslot.py | 33 +++++++-- mellea/stdlib/components/mobject.py | 14 +++- mellea/stdlib/requirements/requirement.py | 12 +++- mellea/stdlib/sampling/budget_forcing.py | 11 ++- 19 files changed, 382 insertions(+), 28 deletions(-) create mode 100644 TASK.md diff --git a/TASK.md b/TASK.md new file mode 100644 index 000000000..7689a71d4 --- /dev/null +++ b/TASK.md @@ -0,0 +1,67 @@ +# Task: Fix Ollama exception propagation in `generate_from_raw` + +## Context + +This branch has partial fixes for issues discovered in #573 (test flakiness on local/M1). +Most test fixes are already done (see git diff). One code fix remains. + +## Remaining work + +### Fix exception propagation in `OllamaModelBackend.generate_from_raw` (`mellea/backends/ollama.py`) + +Currently `asyncio.gather(..., return_exceptions=True)` silently converts any exception into +`ModelOutputThunk(value="")`, storing the error only in `generate_log.extra["error"]` — invisible +to callers. A temporary `FancyLogger.warning()` was added as a diagnostic measure but the +exception is still swallowed. + +**Fix:** remove `return_exceptions=True` so exceptions propagate naturally, consistent with all +other backends. Then remove the `isinstance(response, BaseException)` handling block and the +`FancyLogger.warning()` that was added as a temporary measure. + +This aligns with the approach taken in #580 (fail fast, don't swallow exceptions) and gives +consistent behaviour across all backends. + +## Related issues / PRs + +- #573 — test flakiness (this branch addresses most items) +- #577 — same root cause in streaming path +- #580 — fix for streaming path (`astream()` in `base.py`), now merged +- #432 — original bug report covering all backends; closes when #580 merges + +## What's already done in this branch + +- `researcher.py` — `slow` marker added +- `test_format` — `MAX_NEW_TOKENS` bumped to 1024 (ollama + openai_ollama) +- `test_generate_from_raw_with_format` — stale `xfail` removed, assertions validate all results +- `test_generate_from_raw` — 150s timeout added, tighter assertions, reduced context window (2048) **[caveat: see below]** +- `slow` marker description updated (">1 minute") +- Stale `xfail` removed from `test_litellm_watsonx.py` + +## Caveats / open questions + +### `CONTEXT_WINDOW: 2048` in `test_generate_from_raw` may itself cause failures + +Added to reduce KV cache pressure (32K × 4 parallel = 128K slots → 2K × 4 = 8K). In +theory harmless for simple arithmetic prompts. However a 20-run soak after an extended +test session (machine under sustained load) showed 18/20 failures, with Ollama returning +**empty-body responses** (`response.response == ""`) rather than exceptions or hangs. +The `assert all(r.value for r in results)` assertion was catching these empty responses — +the FancyLogger.warning() did NOT fire (no `BaseException` in the gather results), confirming +the empty string was a legitimate Ollama response, not a caught exception. + +This may be caused by the reduced context window interacting with Ollama's internal state +under load, or simply by machine exhaustion — a cold run after the machine rests is needed +to distinguish. The `CONTEXT_WINDOW: 2048` change should be treated as provisional until +this is resolved. + +### aiohttp unclosed session reference + +TASK.md cites "litellm bug #11657" for the unclosed aiohttp `ClientSession` at suite +teardown — confirm this issue number is correct before referencing it in a PR. + +## Out of scope + +- Unclosed aiohttp `ClientSession` at teardown — blocked on upstream litellm bug #11657 +- `asyncio_default_fixture_loop_scope` pytest-asyncio warning — low priority +- The intermittent Ollama hang under load — still under investigation; fixing exception + propagation will at least make failures visible rather than silent diff --git a/cli/alora/train.py b/cli/alora/train.py index 15bdc53a2..03c82c447 100644 --- a/cli/alora/train.py +++ b/cli/alora/train.py @@ -40,6 +40,21 @@ def load_dataset_from_json(json_path, tokenizer, invocation_prompt): + """Load a JSONL dataset and format it for SFT training. + + Reads ``item``/``label`` pairs from a JSONL file and builds a HuggingFace + ``Dataset`` with ``input`` and ``target`` columns. Each input is formatted as + ``"{item}\\nRequirement: <|end_of_text|>\\n{invocation_prompt}"``. + + Args: + json_path: Path to the JSONL file containing ``item``/``label`` pairs. + tokenizer: HuggingFace tokenizer instance (currently unused, reserved for + future tokenization steps). + invocation_prompt: Invocation string appended to each input prompt. + + Returns: + A HuggingFace ``Dataset`` with ``"input"`` and ``"target"`` string columns. + """ data = [] with open(json_path, encoding="utf-8") as f: for line in f: @@ -59,6 +74,16 @@ def load_dataset_from_json(json_path, tokenizer, invocation_prompt): def formatting_prompts_func(example): + """Concatenate input and target columns for SFT prompt formatting. + + Args: + example: A batch dict with ``"input"`` and ``"target"`` list fields, as + produced by HuggingFace ``Dataset.map`` in batched mode. + + Returns: + A list of strings, each formed by concatenating the ``input`` and + ``target`` values for a single example in the batch. + """ return [ f"{example['input'][i]}{example['target'][i]}" for i in range(len(example["input"])) @@ -66,6 +91,8 @@ def formatting_prompts_func(example): class SaveBestModelCallback(TrainerCallback): + """HuggingFace Trainer callback that saves the adapter at its best validation loss.""" + def __init__(self): self.best_eval_loss = float("inf") @@ -79,6 +106,8 @@ def on_evaluate(self, args, state, control, **kwargs): class SafeSaveTrainer(SFTTrainer): + """SFTTrainer subclass that always saves models with safe serialization enabled.""" + def save_model(self, output_dir: str | None = None, _internal_call: bool = False): if self.model is not None: self.model.save_pretrained(output_dir, safe_serialization=True) @@ -100,6 +129,29 @@ def train_model( max_length: int = 1024, grad_accum: int = 4, ): + """Fine-tune a causal language model to produce a LoRA or aLoRA adapter. + + Loads and 80/20-splits the JSONL dataset, configures PEFT with the specified + adapter type, trains using ``SFTTrainer`` with a best-checkpoint callback, saves + the adapter weights, and removes the PEFT-generated ``README.md`` from the output + directory. + + Args: + dataset_path: Path to the JSONL training dataset file. + base_model: Hugging Face model ID or local path to the base model. + output_file: Destination path for the trained adapter weights. + prompt_file: Optional path to a JSON config file with an + ``"invocation_prompt"`` key. Defaults to the aLoRA invocation token. + adapter: Adapter type to train -- ``"alora"`` (default) or ``"lora"``. + device: Device selection -- ``"auto"``, ``"cpu"``, ``"cuda"``, or + ``"mps"``. + run_name: Name of the training run (passed to ``SFTConfig``). + epochs: Number of training epochs. + learning_rate: Optimizer learning rate. + batch_size: Per-device training batch size. + max_length: Maximum token sequence length. + grad_accum: Gradient accumulation steps. + """ if prompt_file: # load the configurable variable invocation_prompt with open(prompt_file) as f: diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index efb8f112c..233c47b2e 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -23,6 +23,12 @@ # Must maintain declaration order # Newer versions must be declared on the bottom class DecompVersion(StrEnum): + """Available versions of the decomposition pipeline template. + + Newer versions must be declared last to ensure ``latest`` always resolves to + the most recent template. + """ + latest = "latest" v1 = "v1" # v2 = "v2" @@ -252,7 +258,32 @@ def run( ), ] = None, ) -> None: - """Runs the decomposition pipeline.""" + """Decompose a task prompt into subtasks with constraints and dependency metadata. + + Reads the task prompt either from a file or interactively, runs the LLM + decomposition pipeline to produce subtask descriptions, Jinja2 prompt templates, + constraint lists, and dependency metadata, validates variable ordering, then + writes a ``{out_name}.json`` result file and a rendered ``{out_name}.py`` + Python script to the output directory. + + Args: + out_dir: Path to an existing directory where output files are saved. + out_name: Base name (no extension) for the output files. Defaults to + ``"m_decomp_result"``. + prompt_file: Optional path to a raw-text file containing the task prompt. + If omitted, the prompt is collected interactively. + model_id: Model name or ID used for all decomposition pipeline steps. + backend: Inference backend -- ``"ollama"`` or ``"openai"``. + backend_req_timeout: Request timeout in seconds for model inference calls. + backend_endpoint: Base URL of the OpenAI-compatible endpoint. Required + when ``backend="openai"``. + backend_api_key: API key for the configured endpoint. Required when + ``backend="openai"``. + version: Version of the decomposition pipeline template to use. + input_var: Optional list of user-input variable names (e.g. ``"DOC"``). + Each name must be a valid Python identifier. Pass this option + multiple times to define multiple variables. + """ try: from jinja2 import Environment, FileSystemLoader diff --git a/cli/decompose/pipeline.py b/cli/decompose/pipeline.py index 1b760697b..239d5e66b 100644 --- a/cli/decompose/pipeline.py +++ b/cli/decompose/pipeline.py @@ -30,11 +30,15 @@ class ConstraintResult(TypedDict): + """A single constraint paired with its assigned validation strategy.""" + constraint: str validation_strategy: str class DecompSubtasksResult(TypedDict): + """The full structured result for one decomposed subtask.""" + subtask: str tag: str constraints: list[ConstraintResult] @@ -46,6 +50,8 @@ class DecompSubtasksResult(TypedDict): class DecompPipelineResult(TypedDict): + """The complete output of a decomposition pipeline run.""" + original_task_prompt: str subtask_list: list[str] identified_constraints: list[ConstraintResult] @@ -54,6 +60,8 @@ class DecompPipelineResult(TypedDict): class DecompBackend(StrEnum): + """Inference backends supported by the decomposition pipeline.""" + ollama = "ollama" openai = "openai" rits = "rits" @@ -71,6 +79,30 @@ def decompose( backend_endpoint: str | None = None, backend_api_key: str | None = None, ) -> DecompPipelineResult: + """Break a task prompt into structured subtasks using a multi-step LLM pipeline. + + Orchestrates five sequential LLM calls to produce a fully structured + decomposition: subtask listing, constraint extraction, validation strategy + selection, prompt template generation, and per-subtask constraint assignment. + + Args: + task_prompt: Natural-language description of the task to decompose. + user_input_variable: Optional list of variable names that will be + templated into generated prompts as user-provided input data. Pass + ``None`` or an empty list if the task requires no input variables. + model_id: Model name or ID used for all pipeline steps. + backend: Inference backend -- ``"ollama"``, ``"openai"``, or ``"rits"``. + backend_req_timeout: Request timeout in seconds for model inference calls. + backend_endpoint: Base URL of the OpenAI-compatible endpoint. Required + when ``backend`` is ``"openai"`` or ``"rits"``. + backend_api_key: API key for the configured endpoint. Required when + ``backend`` is ``"openai"`` or ``"rits"``. + + Returns: + A ``DecompPipelineResult`` containing the original prompt, subtask list, + identified constraints, and fully annotated subtask objects with prompt + templates, constraint assignments, and dependency information. + """ if user_input_variable is None: user_input_variable = [] diff --git a/cli/decompose/utils.py b/cli/decompose/utils.py index 15bbd3011..e170c447a 100644 --- a/cli/decompose/utils.py +++ b/cli/decompose/utils.py @@ -8,6 +8,18 @@ def validate_filename(candidate_str: str) -> bool: + """Check whether a string is safe to use as an output filename. + + Permits alphanumeric characters, underscores, hyphens, periods, and spaces; + the first character must be alphanumeric, an underscore, or a period. + Also enforces a maximum length of 250 characters. + + Args: + candidate_str: The filename candidate to validate. + + Returns: + ``True`` if the string is a safe, valid filename; ``False`` otherwise. + """ import re # Allows alphanumeric characters, underscore, hyphen, period, and space. diff --git a/cli/eval/commands.py b/cli/eval/commands.py index 17ff56be4..ec778a234 100644 --- a/cli/eval/commands.py +++ b/cli/eval/commands.py @@ -31,6 +31,25 @@ def eval_run( ), continue_on_error: bool = typer.Option(True, "--continue-on-error"), ): + """Run LLM-as-a-judge evaluation on one or more test files. + + Loads test cases from JSON/JSONL files, generates candidate responses using + the specified generation backend, scores them with a judge model, and writes + aggregated results to a file. + + Args: + test_files: Paths to JSON/JSONL files containing test cases. + backend: Generation backend name. + model: Generation model name, or ``None`` for the default. + max_gen_tokens: Maximum tokens to generate for each response. + judge_backend: Judge backend name, or ``None`` to reuse the generation + backend. + judge_model: Judge model name, or ``None`` for the default. + max_judge_tokens: Maximum tokens for the judge model's output. + output_path: File path prefix for the results file. + output_format: Output format -- ``"json"`` or ``"jsonl"``. + continue_on_error: If ``True``, skip failed tests instead of raising. + """ from cli.eval.runner import run_evaluations run_evaluations( diff --git a/cli/m.py b/cli/m.py index b6ea7d95b..4cdb4f7fa 100644 --- a/cli/m.py +++ b/cli/m.py @@ -19,7 +19,12 @@ # Add a default callback for handling the default cli description. @cli.callback() def callback() -> None: - """Perform M Tasks.""" + """Mellea command-line tool for LLM-powered workflows. + + Provides sub-commands for serving models (``m serve``), training and uploading + adapters (``m alora``), decomposing tasks into subtasks (``m decompose``), and + running test-based evaluation pipelines (``m eval``). + """ # Typer assumes that all commands are in the same file/module. diff --git a/mellea/backends/adapters/adapter.py b/mellea/backends/adapters/adapter.py index d1698fffc..edd3d186f 100644 --- a/mellea/backends/adapters/adapter.py +++ b/mellea/backends/adapters/adapter.py @@ -47,7 +47,11 @@ def __init__(self, name: str, adapter_type: AdapterType): class LocalHFAdapter(Adapter): - """Adapter for LocalHFBackends.""" + """Abstract adapter subclass for locally loaded HuggingFace model backends. + + Subclasses must implement ``get_local_hf_path`` to return the filesystem path + from which adapter weights should be loaded given a base model name. + """ @abc.abstractmethod def get_local_hf_path(self, base_model_name: str) -> str: diff --git a/mellea/backends/cache.py b/mellea/backends/cache.py index c2cf2c16e..79668cb04 100644 --- a/mellea/backends/cache.py +++ b/mellea/backends/cache.py @@ -35,7 +35,12 @@ def current_size(self) -> int: class SimpleLRUCache(Cache): - """A simple [LRU](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_(LRU)) cache.""" + """A simple `LRU `_ cache. + + Evicts the least-recently-used entry when capacity is exceeded, optionally + invoking an ``on_evict`` callback (e.g. to free GPU memory). Used by local + HuggingFace backends to store and reuse KV cache state across requests. + """ def __init__(self, capacity: int, on_evict: Callable[[Any], None] | None = None): """Initializes the LRU cache with a certain capacity. diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 264fb77b8..66bb93e6c 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -312,13 +312,14 @@ def convert_tools_to_json(tools: dict[str, AbstractMelleaTool]) -> list[dict]: def json_extraction(text: str) -> Generator[dict, None, None]: - """Yields the next valid json object in a given string. + """Yield the next valid JSON object found in a given string. Args: text: Input string potentially containing one or more JSON objects. - Yields: - Each valid JSON object found in ``text``, in order. + Returns: + A generator that yields each valid JSON object found in ``text``, + in order of appearance. """ index = 0 decoder = json.JSONDecoder() @@ -587,7 +588,12 @@ def validate_tool_arguments( # so that all backends don't need it installed. # https://github.com/ollama/ollama-python/blob/60e7b2f9ce710eeb57ef2986c46ea612ae7516af/ollama/_types.py#L19-L101 class SubscriptableBaseModel(BaseModel): - """Class imported from Ollama.""" + """Pydantic ``BaseModel`` subclass that also supports subscript (``[]``) access. + + Imported from the Ollama Python client. Allows model fields to be accessed + via ``model["field"]`` in addition to ``model.field``, which is required for + compatibility with Ollama's internal response parsing. + """ def __getitem__(self, key: str) -> Any: """Getitem. diff --git a/mellea/core/backend.py b/mellea/core/backend.py index e08eb15da..7f971823c 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -40,7 +40,13 @@ class Backend(abc.ABC): - """An abstract `Backend`.""" + """Abstract base class for all inference backends. + + All concrete backends must implement ``generate_from_context`` (context-aware + single-action generation) and ``generate_from_raw`` (context-free batch + generation). The ``do_generate_walk`` / ``do_generate_walks`` helpers can be + used to pre-compute any unresolved ``ModelOutputThunk`` leaves before rendering. + """ @final async def generate_from_context( @@ -186,7 +192,15 @@ async def do_generate_walks( def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputThunk]: - """Returns the generation walk ordering for a Span.""" + """Return all uncomputed ``ModelOutputThunk`` leaves reachable from ``c``. + + Args: + c: A ``CBlock``, ``Component``, or ``ModelOutputThunk`` to traverse. + + Returns: + A flat list of uncomputed ``ModelOutputThunk`` instances in the order + they need to be resolved (depth-first over ``Component.parts()``). + """ match c: case ModelOutputThunk() if not c.is_computed(): return [c] diff --git a/mellea/core/base.py b/mellea/core/base.py index 10fdb01ef..706bca72a 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -703,7 +703,14 @@ def call_func(self) -> Any: def blockify(s: str | CBlock | Component) -> CBlock | Component: - """`blockify` is a helper function that turns raw strings into CBlocks.""" + """Turn a raw string into a ``CBlock``, leaving ``CBlock`` and ``Component`` objects unchanged. + + Args: + s: A plain string, ``CBlock``, or ``Component`` to normalise. + + Returns: + A ``CBlock`` wrapping ``s`` if it was a string; otherwise ``s`` unchanged. + """ # noinspection PyUnreachableCode match s: case str(): @@ -717,7 +724,15 @@ def blockify(s: str | CBlock | Component) -> CBlock | Component: def get_images_from_component(c: Component) -> None | list[ImageBlock]: - """Gets images from a `Component` if they are present and a non-empty list, otherwise returns None.""" + """Return the images attached to a ``Component``, or ``None`` if absent or empty. + + Args: + c: The ``Component`` whose ``images`` attribute is inspected. + + Returns: + A non-empty list of ``ImageBlock`` objects if the component has an + ``images`` attribute with at least one element; ``None`` otherwise. + """ if hasattr(c, "images"): imgs = c.images # type: ignore if imgs is not None: diff --git a/mellea/core/utils.py b/mellea/core/utils.py index 97249cccd..aa4c5d813 100644 --- a/mellea/core/utils.py +++ b/mellea/core/utils.py @@ -15,7 +15,12 @@ class RESTHandler(logging.Handler): - """RESTHandler for logging.""" + """Logging handler that forwards records to a local REST endpoint. + + Sends log records as JSON to ``/api/receive`` when the ``FLOG`` environment + variable is set. Failures are silently suppressed to avoid disrupting the + application. + """ def __init__( self, api_url: str, method: str = "POST", headers: dict[str, str] | None = None @@ -44,7 +49,11 @@ def emit(self, record: logging.LogRecord) -> None: class JsonFormatter(logging.Formatter): - """Logging formatter for JSON.""" + """Logging formatter that serialises log records as structured JSON dicts. + + Includes timestamp, level, message, module, function name, line number, + process ID, thread ID, and (if present) exception information. + """ def format(self, record): # type: ignore """Formats record as a JSON serializable object.""" @@ -90,7 +99,13 @@ def format(self, record: logging.LogRecord) -> str: class FancyLogger: - """A fancy logger.""" + """Singleton logger with colour-coded console output and optional REST forwarding. + + Obtain the shared logger instance via ``FancyLogger.get_logger()``. Log level + defaults to ``INFO`` but can be raised to ``DEBUG`` by setting the ``DEBUG`` + environment variable. When the ``FLOG`` environment variable is set, records are + also forwarded to a local ``/api/receive`` REST endpoint via ``RESTHandler``. + """ logger = None diff --git a/mellea/helpers/openai_compatible_helpers.py b/mellea/helpers/openai_compatible_helpers.py index 783ce8c7f..739677368 100644 --- a/mellea/helpers/openai_compatible_helpers.py +++ b/mellea/helpers/openai_compatible_helpers.py @@ -141,7 +141,16 @@ def chat_completion_delta_merge( def message_to_openai_message(msg: Message): - """Serializes a mellea Message object to the message format required by OpenAI compatible api providers.""" + """Serialise a Mellea ``Message`` to the format required by OpenAI-compatible API providers. + + Args: + msg: The ``Message`` object to serialise. + + Returns: + A dict with ``"role"`` and ``"content"`` fields. When the message carries + images, ``"content"`` is a list of text and image-URL dicts; otherwise it + is a plain string. + """ if msg.images is not None: img_list = [ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img}"}} @@ -173,7 +182,15 @@ def message_to_openai_message(msg: Message): def messages_to_docs(msgs: list[Message]) -> list[dict[str, str]]: - """Extracts the docs from a list of messages.""" + """Extract all ``Document`` objects from a list of ``Message`` objects. + + Args: + msgs: List of ``Message`` objects whose ``_docs`` attributes are inspected. + + Returns: + A list of dicts, each with a ``"text"`` key and optional ``"title"`` and + ``"doc_id"`` keys, suitable for passing to an OpenAI-compatible RAG API. + """ docs: list[Document] = [] for message in msgs: if message._docs is not None: diff --git a/mellea/stdlib/components/docs/richdocument.py b/mellea/stdlib/components/docs/richdocument.py index 2cfe06970..44444fbd2 100644 --- a/mellea/stdlib/components/docs/richdocument.py +++ b/mellea/stdlib/components/docs/richdocument.py @@ -98,7 +98,11 @@ def from_document_file(cls, source: str | Path | DocumentStream) -> RichDocument class TableQuery(Query): - """Table-specific query.""" + """A ``Query`` component specialised for ``Table`` objects. + + Formats the table as Markdown alongside the query string so the LLM receives + both the structured table content and the natural-language question. + """ def __init__(self, obj: Table, query: str) -> None: """Initializes a new instance of the `TableQuery` class. @@ -129,7 +133,11 @@ def format_for_llm(self) -> TemplateRepresentation: class TableTransform(Transform): - """Table-specific transform.""" + """A ``Transform`` component specialised for ``Table`` objects. + + Formats the table as Markdown alongside the transformation instruction so the + LLM receives both the structured table content and the mutation description. + """ def __init__(self, obj: Table, transformation: str) -> None: """Initializes a new instance of the `TableTransform` class. diff --git a/mellea/stdlib/components/genslot.py b/mellea/stdlib/components/genslot.py index eff9ae753..686e6f063 100644 --- a/mellea/stdlib/components/genslot.py +++ b/mellea/stdlib/components/genslot.py @@ -77,7 +77,7 @@ class ArgumentDict(TypedDict): class Argument: - """An Argument.""" + """A single function argument with its name, type annotation, and value.""" def __init__( self, @@ -94,6 +94,12 @@ def __init__( class Arguments(CBlock): + """A ``CBlock`` that renders a list of ``Argument`` objects as human-readable text. + + Each argument is formatted as ``"- name: value (type: annotation)"`` and the + items are newline-joined into a single string suitable for inclusion in a prompt. + """ + def __init__(self, arguments: list[Argument]): """Create a textual representation of a list of arguments.""" # Make meta the original list of arguments and create a list of textual representations. @@ -143,10 +149,15 @@ def __init__( class Function(Generic[P, R]): - """A Function.""" + """Wraps a callable with its introspected ``FunctionDict`` metadata. + + Stores the original callable alongside its name, signature, and docstring + as produced by ``describe_function``, so generative slots can render them + into prompts without re-inspecting the function each time. + """ def __init__(self, func: Callable[P, R]): - """A Function.""" + """Wrap a callable and capture its metadata via ``describe_function``.""" self._func: Callable[P, R] = func self._function_dict: FunctionDict = describe_function(func) @@ -258,7 +269,14 @@ def __init__(self): class GenerativeSlot(Component[R], Generic[P, R]): - """A generative slot component.""" + """Abstract base class for AI-powered function wrappers produced by ``@generative``. + + A ``GenerativeSlot`` wraps a callable and uses an LLM to generate its output. + Subclasses (``SyncGenerativeSlot``, ``AsyncGenerativeSlot``) implement + ``__call__`` for synchronous and asynchronous invocation respectively. + The function's signature, docstring, and type hints are rendered into a prompt + so the LLM can imitate the function's intended behaviour. + """ def __init__(self, func: Callable[P, R]): """A generative slot function that converts a given `func` to a generative slot. @@ -424,6 +442,13 @@ def _parse(self, computed: ModelOutputThunk) -> R: class SyncGenerativeSlot(GenerativeSlot, Generic[P, R]): + """A synchronous generative slot that blocks until the LLM response is ready. + + Returned by ``@generative`` when the decorated function is not a coroutine. + ``__call__`` returns the parsed result directly (when a session is passed) or a + ``(result, context)`` tuple (when a context and backend are passed). + """ + @overload def __call__( self, diff --git a/mellea/stdlib/components/mobject.py b/mellea/stdlib/components/mobject.py index 3df8e8a72..d8e10c119 100644 --- a/mellea/stdlib/components/mobject.py +++ b/mellea/stdlib/components/mobject.py @@ -19,7 +19,12 @@ class Query(Component[str]): - """A Query component.""" + """A ``Component`` that pairs an ``MObject`` with a natural-language question. + + Wraps the object and its query string into a ``TemplateRepresentation`` so the + formatter can render both together in a prompt, optionally forwarding the + object's tools and fields to the template. + """ def __init__(self, obj: Component, query: str) -> None: """Initializes a new instance of Query with the provided object and query. @@ -63,7 +68,12 @@ def _parse(self, computed: ModelOutputThunk) -> str: class Transform(Component[str]): - """A Transform component.""" + """A ``Component`` that pairs an ``MObject`` with a natural-language mutation instruction. + + Wraps the object and its transformation description into a + ``TemplateRepresentation`` so the formatter can render both together in a prompt, + optionally forwarding the object's tools and fields to the template. + """ def __init__(self, obj: Component, transformation: str) -> None: """Initializes a new instance of Transform with the provided object and transformation description. diff --git a/mellea/stdlib/requirements/requirement.py b/mellea/stdlib/requirements/requirement.py index 09e7a1a60..3db2a8b41 100644 --- a/mellea/stdlib/requirements/requirement.py +++ b/mellea/stdlib/requirements/requirement.py @@ -90,7 +90,11 @@ def reqify(r: str | Requirement) -> Requirement: def req(*args, **kwargs) -> Requirement: - """Shorthand for Requirement.__init__. + """Shorthand for ``Requirement.__init__``. + + Args: + *args: Positional arguments forwarded to ``Requirement.__init__``. + **kwargs: Keyword arguments forwarded to ``Requirement.__init__``. Returns: A new ``Requirement`` instance. @@ -99,7 +103,11 @@ def req(*args, **kwargs) -> Requirement: def check(*args, **kwargs) -> Requirement: - """Shorthand for Requirement.__init__(..., check_only=True). + """Shorthand for ``Requirement.__init__(..., check_only=True)``. + + Args: + *args: Positional arguments forwarded to ``Requirement.__init__``. + **kwargs: Keyword arguments forwarded to ``Requirement.__init__``. Returns: A new ``Requirement`` instance with ``check_only=True``. diff --git a/mellea/stdlib/sampling/budget_forcing.py b/mellea/stdlib/sampling/budget_forcing.py index f59aa85b3..6515fb8e8 100644 --- a/mellea/stdlib/sampling/budget_forcing.py +++ b/mellea/stdlib/sampling/budget_forcing.py @@ -23,7 +23,16 @@ class BudgetForcingSamplingStrategy(RejectionSamplingStrategy): - """Budget forcing sampling class.""" + """Sampling strategy that enforces a token budget for chain-of-thought reasoning. + + Extends ``RejectionSamplingStrategy`` with explicit control over the ```` + block size and the answer block size. On each loop iteration, + ``think_budget_forcing`` interleaves forced-thinking and final-answer generation, + after which the standard rejection-sampling validation pass determines whether to + accept or retry. + + Currently only supports the ``OllamaModelBackend``. + """ think_max_tokens: int | None answer_max_tokens: int | None From df95496b62b30469fce6c171b0bbd741a9679ea7 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Wed, 11 Mar 2026 11:03:59 +0000 Subject: [PATCH 05/14] docs: add missing Args: sections to class-level docstrings (#613) Add Args: sections to class docstrings that had Attributes: but no Args:, completing Google-style docstring coverage: - core/requirement.py: ValidationResult, Requirement - core/sampling.py: SamplingResult - core/base.py: CBlock, ImageBlock, ModelOutputThunk, ContextTurn, TemplateRepresentation, GenerateLog, ModelToolCall - formatters/template_formatter.py: TemplateFormatter - formatters/granite/intrinsics/input.py: IntrinsicsRewriter - formatters/granite/intrinsics/output.py: TransformationRule, TokenToFloat, DecodeSentences, MergeSpans - formatters/granite/retrievers/embeddings.py: InMemoryRetriever - stdlib/components/chat.py: Message, ToolMessage - helpers/async_helpers.py: ClientCache --- cli/alora/commands.py | 4 + cli/alora/intrinsic_uploader.py | 30 ++ cli/alora/readme_generator.py | 12 + cli/alora/train.py | 41 ++- cli/alora/upload.py | 6 +- cli/decompose/decompose.py | 13 + cli/decompose/pipeline.py | 49 ++- cli/eval/runner.py | 55 ++- docs/docs/docs.json | 7 + mellea/backends/adapters/adapter.py | 141 ++++++-- mellea/backends/adapters/catalog.py | 25 +- mellea/backends/backend.py | 11 + mellea/backends/bedrock.py | 20 +- mellea/backends/cache.py | 61 +++- mellea/backends/dummy.py | 40 ++- mellea/backends/huggingface.py | 176 +++++++++- mellea/backends/litellm.py | 83 ++++- mellea/backends/model_ids.py | 18 + mellea/backends/model_options.py | 47 ++- mellea/backends/ollama.py | 101 +++++- mellea/backends/openai.py | 146 +++++++- mellea/backends/tools.py | 101 +++++- mellea/backends/vllm.py | 77 ++++- mellea/backends/watsonx.py | 120 ++++++- mellea/core/backend.py | 25 +- mellea/core/base.py | 322 ++++++++++++++++-- mellea/core/formatter.py | 9 +- mellea/core/requirement.py | 69 +++- mellea/core/sampling.py | 21 +- mellea/core/utils.py | 54 ++- mellea/formatters/chat_formatter.py | 17 +- mellea/formatters/granite/base/io.py | 89 +++-- mellea/formatters/granite/base/optional.py | 4 + mellea/formatters/granite/base/types.py | 129 ++++++- mellea/formatters/granite/base/util.py | 17 + .../granite/granite3/granite32/input.py | 33 +- .../granite/granite3/granite32/output.py | 21 +- .../granite/granite3/granite33/input.py | 31 +- .../granite/granite3/granite33/output.py | 19 +- mellea/formatters/granite/granite3/types.py | 90 ++++- mellea/formatters/granite/intrinsics/input.py | 47 ++- .../granite/intrinsics/json_util.py | 63 +++- .../formatters/granite/intrinsics/output.py | 240 +++++++++++-- mellea/formatters/granite/intrinsics/util.py | 9 + .../granite/retrievers/elasticsearch.py | 45 ++- .../granite/retrievers/embeddings.py | 10 +- mellea/formatters/granite/retrievers/util.py | 12 + mellea/formatters/template_formatter.py | 52 ++- mellea/helpers/async_helpers.py | 7 + mellea/stdlib/components/chat.py | 51 ++- mellea/stdlib/components/docs/document.py | 32 +- mellea/stdlib/components/docs/richdocument.py | 182 ++++++++-- mellea/stdlib/components/genslot.py | 147 +++++++- mellea/stdlib/components/instruction.py | 50 ++- .../stdlib/components/intrinsic/intrinsic.py | 50 ++- mellea/stdlib/components/mify.py | 87 +++-- mellea/stdlib/components/mobject.py | 148 ++++++-- mellea/stdlib/components/react.py | 32 +- mellea/stdlib/components/simple.py | 32 +- mellea/stdlib/components/unit_test_eval.py | 110 +++++- mellea/stdlib/context.py | 55 ++- mellea/stdlib/requirements/python_reqs.py | 19 +- mellea/stdlib/requirements/requirement.py | 31 +- mellea/stdlib/requirements/safety/guardian.py | 71 +++- mellea/stdlib/sampling/base.py | 15 +- mellea/stdlib/sampling/budget_forcing.py | 34 ++ mellea/stdlib/sampling/majority_voting.py | 112 +++++- .../sampling_algos/budget_forcing_alg.py | 6 +- mellea/stdlib/sampling/sofai.py | 35 +- mellea/stdlib/session.py | 45 ++- mellea/stdlib/tools/interpreter.py | 93 ++++- 71 files changed, 3792 insertions(+), 464 deletions(-) diff --git a/cli/alora/commands.py b/cli/alora/commands.py index c3cd041fc..4b2f537e3 100644 --- a/cli/alora/commands.py +++ b/cli/alora/commands.py @@ -147,6 +147,10 @@ def alora_add_readme( name: Destination model name on Hugging Face Hub. hints: Path to a file containing additional domain hints, or ``None``. io_yaml: Path to the ``io.yaml`` intrinsic configuration file, or ``None``. + + Raises: + OSError: If no Hugging Face authentication token is found. + SystemExit: If the user declines to upload the generated README. """ from huggingface_hub import HfFolder, create_repo, upload_file diff --git a/cli/alora/intrinsic_uploader.py b/cli/alora/intrinsic_uploader.py index 43f5a3408..fe7c52a88 100644 --- a/cli/alora/intrinsic_uploader.py +++ b/cli/alora/intrinsic_uploader.py @@ -24,6 +24,36 @@ def upload_intrinsic( io_yaml: str, private: bool = True, ): + """Upload an adapter to Hugging Face Hub using the intrinsic directory layout. + + Creates or updates a private Hugging Face repository and uploads adapter + weights into a ``//`` sub-directory, + together with the ``io.yaml`` configuration file. If an + ``INTRINSIC_README.md`` exists in the weight directory it is also uploaded + as the repository root ``README.md``. + + Args: + weight_path (str): Local directory containing the adapter weights + (output of ``save_pretrained``). + model_name (str): Target Hugging Face repository name in + ``"/"`` format (e.g. ``"acme/carbchecker-alora"``). + base_model (str): Base model ID or path (e.g. + ``"ibm-granite/granite-3.3-2b-instruct"``). Must contain at most + one ``"/"`` separator. + type (Literal["lora", "alora"]): Adapter type, used as the leaf + directory name in the repository layout. + io_yaml (str): Path to the ``io.yaml`` configuration file for + intrinsic input/output processing. + private (bool): Whether the repository should be private. Currently + only ``True`` is supported. + + Raises: + AssertionError: If ``weight_path`` or ``io_yaml`` do not exist, if + ``private`` is ``False``, if ``base_model`` contains more than one + ``"/"`` separator, or if ``model_name`` does not contain exactly + one ``"/"`` separator. + OSError: If no Hugging Face authentication token is found. + """ try: assert os.path.exists(weight_path) assert os.path.exists(io_yaml) diff --git a/cli/alora/readme_generator.py b/cli/alora/readme_generator.py index f0a5e80d4..de2643e20 100644 --- a/cli/alora/readme_generator.py +++ b/cli/alora/readme_generator.py @@ -22,6 +22,18 @@ class ReadmeTemplateVars(BaseModel): + """Pydantic model holding all variables required to render the intrinsic README template. + + Attributes: + high_level_description (str): A 2-3 sentence description of what the intrinsic adapter does. + dataset_description (str): Brief description of the training dataset contents and format. + userid (str): HuggingFace user ID (the namespace portion of the model name). + intrinsic_name (str): Short snake_case identifier for the intrinsic (e.g. ``"carbchecker"``). + intrinsic_name_camelcase (str): CamelCase version of ``intrinsic_name`` (e.g. ``"CarbChecker"``). + arglist (str): Python function argument list with type hints (e.g. ``"description: str"``). + arglist_without_type_annotations (str): Argument list without type hints (e.g. ``"description"``). + """ + high_level_description: str dataset_description: str userid: str diff --git a/cli/alora/train.py b/cli/alora/train.py index 03c82c447..bda54ae39 100644 --- a/cli/alora/train.py +++ b/cli/alora/train.py @@ -91,12 +91,33 @@ def formatting_prompts_func(example): class SaveBestModelCallback(TrainerCallback): - """HuggingFace Trainer callback that saves the adapter at its best validation loss.""" + """HuggingFace Trainer callback that saves the adapter at its best validation loss. + + Attributes: + best_eval_loss (float): Lowest evaluation loss seen so far across all + evaluation steps. Initialised to ``float("inf")``. + """ def __init__(self): self.best_eval_loss = float("inf") def on_evaluate(self, args, state, control, **kwargs): + """Save the adapter weights if the current evaluation loss is a new best. + + Called automatically by the HuggingFace Trainer after each evaluation + step. Compares the current ``eval_loss`` from ``metrics`` against + ``best_eval_loss`` and, if lower, updates the stored best and saves the + model to ``args.output_dir``. + + Args: + args: ``TrainingArguments`` instance with training configuration, + including ``output_dir``. + state: ``TrainerState`` instance with the current training state. + control: ``TrainerControl`` instance for controlling training flow. + **kwargs: Additional keyword arguments provided by the Trainer, + including ``"model"`` (the current PEFT model) and + ``"metrics"`` (a dict containing ``"eval_loss"``). + """ model = kwargs["model"] metrics = kwargs["metrics"] eval_loss = metrics.get("eval_loss") @@ -109,6 +130,18 @@ class SafeSaveTrainer(SFTTrainer): """SFTTrainer subclass that always saves models with safe serialization enabled.""" def save_model(self, output_dir: str | None = None, _internal_call: bool = False): + """Save the model and tokenizer with safe serialization always enabled. + + Overrides ``SFTTrainer.save_model`` to call ``save_pretrained`` with + ``safe_serialization=True``, ensuring weights are saved in safetensors + format rather than the legacy pickle-based format. + + Args: + output_dir (str | None): Directory to save the model into. If + ``None``, the trainer's configured ``output_dir`` is used. + _internal_call (bool): Internal flag passed through from the Trainer + base class; not used by this override. + """ if self.model is not None: self.model.save_pretrained(output_dir, safe_serialization=True) if self.tokenizer is not None: @@ -151,6 +184,12 @@ def train_model( batch_size: Per-device training batch size. max_length: Maximum token sequence length. grad_accum: Gradient accumulation steps. + + Raises: + ValueError: If ``device`` is not one of ``"auto"``, ``"cpu"``, + ``"cuda"``, or ``"mps"``. + RuntimeError: If the GPU has insufficient VRAM to load the model + (wraps ``NotImplementedError`` for meta tensor errors). """ if prompt_file: # load the configurable variable invocation_prompt diff --git a/cli/alora/upload.py b/cli/alora/upload.py index 09300e420..53425b582 100644 --- a/cli/alora/upload.py +++ b/cli/alora/upload.py @@ -19,8 +19,10 @@ def upload_model(weight_path: str, model_name: str, private: bool = True): model_name (str): Target model repo name (e.g., "acme/carbchecker-alora"). private (bool): Whether the repo should be private. Default: True. - Requires: - - `HF_TOKEN` set in environment or via `huggingface-cli login`. + Raises: + FileNotFoundError: If ``weight_path`` does not exist on disk. + OSError: If no Hugging Face authentication token is found. + RuntimeError: If creating or accessing the Hugging Face repository fails. """ if not os.path.exists(weight_path): raise FileNotFoundError(f"Adapter directory not found: {weight_path}") diff --git a/cli/decompose/decompose.py b/cli/decompose/decompose.py index 233c47b2e..4e3f0112f 100644 --- a/cli/decompose/decompose.py +++ b/cli/decompose/decompose.py @@ -27,6 +27,10 @@ class DecompVersion(StrEnum): Newer versions must be declared last to ensure ``latest`` always resolves to the most recent template. + + Attributes: + latest (str): Sentinel value that resolves to the last declared version. + v1 (str): Version 1 of the decomposition pipeline template. """ latest = "latest" @@ -283,6 +287,15 @@ def run( input_var: Optional list of user-input variable names (e.g. ``"DOC"``). Each name must be a valid Python identifier. Pass this option multiple times to define multiple variables. + + Raises: + AssertionError: If ``out_name`` contains invalid characters, if + ``out_dir`` does not exist or is not a directory, or if any + ``input_var`` name is not a valid Python identifier. + ValueError: If a required input variable is missing from ``input_var`` + or if circular dependencies are detected among subtasks. + Exception: Re-raised from the decomposition pipeline after cleaning up + any partially written output files. """ try: from jinja2 import Environment, FileSystemLoader diff --git a/cli/decompose/pipeline.py b/cli/decompose/pipeline.py index 239d5e66b..85344da76 100644 --- a/cli/decompose/pipeline.py +++ b/cli/decompose/pipeline.py @@ -30,14 +30,36 @@ class ConstraintResult(TypedDict): - """A single constraint paired with its assigned validation strategy.""" + """A single constraint paired with its assigned validation strategy. + + Attributes: + constraint (str): Natural-language description of the constraint. + validation_strategy (str): Strategy assigned to validate the constraint; + either ``"code"`` or ``"llm"``. + """ constraint: str validation_strategy: str class DecompSubtasksResult(TypedDict): - """The full structured result for one decomposed subtask.""" + """The full structured result for one decomposed subtask. + + Attributes: + subtask (str): Natural-language description of the subtask. + tag (str): Short identifier for the subtask, used as a variable name + in Jinja2 templates and dependency references. + constraints (list[ConstraintResult]): List of constraints assigned to + this subtask, each with a validation strategy. + prompt_template (str): Jinja2 prompt template string for this subtask, + with ``{{ variable }}`` placeholders for inputs and prior subtask results. + input_vars_required (list[str]): Ordered list of user-provided input + variable names referenced in ``prompt_template``. + depends_on (list[str]): Ordered list of subtask tags whose results are + referenced in ``prompt_template``. + generated_response (str): Optional field holding the model response + produced during execution; not present until the subtask runs. + """ subtask: str tag: str @@ -50,7 +72,20 @@ class DecompSubtasksResult(TypedDict): class DecompPipelineResult(TypedDict): - """The complete output of a decomposition pipeline run.""" + """The complete output of a decomposition pipeline run. + + Attributes: + original_task_prompt (str): The raw task prompt provided by the user. + subtask_list (list[str]): Ordered list of subtask descriptions produced + by the subtask-listing stage. + identified_constraints (list[ConstraintResult]): Constraints extracted + from the original task prompt, each with a validation strategy. + subtasks (list[DecompSubtasksResult]): Fully annotated subtask objects + with prompt templates, constraint assignments, and dependency + information. + final_response (str): Optional field holding the aggregated final + response produced during execution; not present until the pipeline runs. + """ original_task_prompt: str subtask_list: list[str] @@ -60,7 +95,13 @@ class DecompPipelineResult(TypedDict): class DecompBackend(StrEnum): - """Inference backends supported by the decomposition pipeline.""" + """Inference backends supported by the decomposition pipeline. + + Attributes: + ollama (str): Local Ollama inference server backend. + openai (str): Any OpenAI-compatible HTTP endpoint backend. + rits (str): IBM RITS (Remote Inference and Training Service) backend. + """ ollama = "ollama" openai = "openai" diff --git a/cli/eval/runner.py b/cli/eval/runner.py index 5d2b17c9b..77bb4a3c8 100644 --- a/cli/eval/runner.py +++ b/cli/eval/runner.py @@ -24,7 +24,22 @@ class InputEvalResult: - """Store results of a single input evaluation (within a unit test).""" + """Store results of a single input evaluation (within a unit test). + + Args: + input_text (str): The raw input text sent to the generation model. + model_output (str): The text response produced by the generation model. + validation_passed (bool): Whether the judge scored this response as passing. + score (int): Numeric score assigned by the judge (``1`` for pass, ``0`` for fail). + validation_reason (str): Justification text returned by the judge model. + + Attributes: + input_text (str): The raw input text sent to the generation model. + model_output (str): The text response produced by the generation model. + validation_passed (bool): Whether the judge scored this response as passing. + score (int): Numeric score assigned by the judge (``1`` for pass, ``0`` for fail). + validation_reason (str): Justification text returned by the judge model. + """ def __init__( self, @@ -41,6 +56,12 @@ def __init__( self.validation_reason = validation_reason def to_dict(self): + """Serialise the input evaluation result to a plain dictionary. + + Returns: + dict: A dictionary with keys ``"input"``, ``"model_output"``, + ``"passed"``, ``"score"``, and ``"justification"``. + """ return { "input": self.input_text, "model_output": self.model_output, @@ -51,13 +72,38 @@ def to_dict(self): class TestEvalResult: - """Store results of a single test evaluation.""" + """Store results of a single test evaluation. + + Args: + test_eval (TestBasedEval): The unit test specification containing + the test ID, name, instructions, inputs, and expected targets. + input_results (list[InputEvalResult]): Per-input evaluation outcomes + produced by running the generation and judge models. + + Attributes: + test_eval (TestBasedEval): The unit test specification containing + the test ID, name, instructions, inputs, and expected targets. + input_results (list[InputEvalResult]): Per-input evaluation outcomes + produced by running the generation and judge models. + passed_count (int): Number of inputs that received a passing score. + total_count (int): Total number of inputs evaluated. + pass_rate (float): Fraction of inputs that passed (``passed_count / total_count``). + """ def __init__(self, test_eval: TestBasedEval, input_results: list[InputEvalResult]): self.test_eval = test_eval self.input_results = input_results def to_dict(self): + """Serialise the test evaluation result to a plain dictionary. + + Returns: + dict: A dictionary containing the test metadata (``"test_id"``, + ``"source"``, ``"name"``, ``"instructions"``), per-input results + under ``"input_results"``, expected targets under + ``"expected_targets"``, and summary counts (``"passed"``, + ``"total_count"``, ``"pass_rate"``). + """ return { "test_id": self.test_eval.test_id, "source": self.test_eval.source, @@ -98,6 +144,11 @@ def create_session( Returns: A configured ``MelleaSession`` ready for generation. + + Raises: + ValueError: If ``backend`` is not one of the supported backend names. + Exception: Re-raised from backend or session construction if + initialisation fails. """ model_id = None if model: diff --git a/docs/docs/docs.json b/docs/docs/docs.json index 37dd7ea40..af5aaa45c 100644 --- a/docs/docs/docs.json +++ b/docs/docs/docs.json @@ -152,6 +152,7 @@ "group": "backends", "pages": [ "api/mellea/backends/backend", + "api/mellea/backends/backends", "api/mellea/backends/bedrock", "api/mellea/backends/cache", "api/mellea/backends/dummy", @@ -180,6 +181,7 @@ "pages": [ "api/mellea/core/backend", "api/mellea/core/base", + "api/mellea/core/core", "api/mellea/core/formatter", "api/mellea/core/requirement", "api/mellea/core/sampling", @@ -190,6 +192,7 @@ "group": "formatters", "pages": [ "api/mellea/formatters/chat_formatter", + "api/mellea/formatters/formatters", "api/mellea/formatters/template_formatter", { "group": "granite", @@ -254,6 +257,7 @@ "group": "helpers", "pages": [ "api/mellea/helpers/async_helpers", + "api/mellea/helpers/helpers", "api/mellea/helpers/openai_compatible_helpers", "api/mellea/helpers/server_type" ] @@ -264,6 +268,7 @@ "api/mellea/stdlib/context", "api/mellea/stdlib/functional", "api/mellea/stdlib/session", + "api/mellea/stdlib/stdlib", { "group": "components", "pages": [ @@ -363,6 +368,7 @@ { "group": "decompose", "pages": [ + "api/cli/decompose/__init__", "api/cli/decompose/decompose", "api/cli/decompose/pipeline", "api/cli/decompose/utils" @@ -372,6 +378,7 @@ "group": "eval", "pages": [ "api/cli/eval/commands", + "api/cli/eval/eval", "api/cli/eval/runner" ] } diff --git a/mellea/backends/adapters/adapter.py b/mellea/backends/adapters/adapter.py index edd3d186f..9acbfa0da 100644 --- a/mellea/backends/adapters/adapter.py +++ b/mellea/backends/adapters/adapter.py @@ -22,7 +22,26 @@ class Adapter(abc.ABC): - """An adapter that can be added to a single backend.""" + """An adapter that can be added to a single backend. + + An adapter can only be registered with one backend at a time. Use + ``adapter.qualified_name`` when referencing the adapter after adding it. + + Args: + name (str): Human-readable name of the adapter. + adapter_type (AdapterType): Enum describing the adapter type (e.g. + ``AdapterType.LORA`` or ``AdapterType.ALORA``). + + Attributes: + name (str): Human-readable name of the adapter. + adapter_type (AdapterType): Adapter type (``LORA`` or ``ALORA``). + qualified_name (str): Unique name used for loading and lookup; formed + as ``"_"``. + backend (Backend | None): The backend this adapter has been added to, + or ``None`` if not yet added. + path (str | None): Filesystem path to the adapter weights; set when + the adapter is added to a backend. + """ def __init__(self, name: str, adapter_type: AdapterType): """An adapter that can be added to a backend. @@ -55,10 +74,14 @@ class LocalHFAdapter(Adapter): @abc.abstractmethod def get_local_hf_path(self, base_model_name: str) -> str: - """Returns the path needed to load the adapter. + """Return the local filesystem path from which adapter weights should be loaded. Args: - base_model_name: the base model; typically the last part of the Hugging Face model id like "granite-4.0-micro" + base_model_name (str): The base model name; typically the last component + of the HuggingFace model ID (e.g. ``"granite-4.0-micro"``). + + Returns: + str: Filesystem path to the adapter weights directory. """ ... @@ -67,11 +90,33 @@ class IntrinsicAdapter(LocalHFAdapter): """Base class for adapters that implement intrinsics. Subtype of :class:`Adapter` for models that: + * implement intrinsic functions * are packaged as LoRA or aLoRA adapters on top of a base model * use the shared model loading code in ``mellea.formatters.granite.intrinsics`` * use the shared input and output processing code in ``mellea.formatters.granite.intrinsics`` + + Args: + intrinsic_name (str): Name of the intrinsic (e.g. ``"answerability"``); the + adapter's ``qualified_name`` will be derived from this. + adapter_type (AdapterType): Enum describing the adapter type; defaults to + ``AdapterType.ALORA``. + config_file (str | pathlib.Path | None): Path to a YAML config file defining + the intrinsic's I/O transformations; mutually exclusive with + ``config_dict``. + config_dict (dict | None): Dict defining the intrinsic's I/O transformations; + mutually exclusive with ``config_file``. + base_model_name (str | None): Base model name used to look up the I/O + processing config when neither ``config_file`` nor ``config_dict`` are + provided. + + Attributes: + intrinsic_name (str): Name of the intrinsic this adapter implements. + intrinsic_metadata (IntriniscsCatalogEntry): Catalog metadata for the intrinsic. + base_model_name (str | None): Base model name provided at construction, if any. + adapter_type (AdapterType): The adapter type (``LORA`` or ``ALORA``). + config (dict): Parsed I/O transformation configuration for the intrinsic. """ def __init__( @@ -150,11 +195,16 @@ def __init__( self.config: dict = config_dict def get_local_hf_path(self, base_model_name: str) -> str: - """Returns the path needed to load the adapter. + """Return the local filesystem path from which adapter weights should be loaded. + + Downloads the adapter weights if they are not already cached locally. Args: - base_model_name: the base model; typically the last part of the huggingface - model id like "granite-3.3-8b-instruct" + base_model_name (str): The base model name; typically the last component + of the HuggingFace model ID (e.g. ``"granite-3.3-8b-instruct"``). + + Returns: + str: Filesystem path to the downloaded adapter weights directory. """ return self.download_and_get_path(base_model_name) @@ -187,17 +237,18 @@ def get_adapter_for_intrinsic( intrinsic_adapter_types: list[AdapterType] | tuple[AdapterType, ...], available_adapters: dict[str, T], ) -> T | None: - """Finds an adapter from a dict of available adapters based on the intrinsic name and its allowed adapter types. + """Find an adapter from a dict of available adapters based on the intrinsic name and its allowed adapter types. Args: - repo_id: Name of Hugging Face Hub repository containing the adapters that - implement the intrinsic - intrinsic_name: the name of the intrinsic, like "answerability" - intrinsic_adapter_types: the adapter types allowed for this intrinsic, like ALORA / LORA - available_adapters: the available adapters to choose from; maps adapter.qualified_name to the Adapter + intrinsic_name (str): The name of the intrinsic, e.g. ``"answerability"``. + intrinsic_adapter_types (list[AdapterType] | tuple[AdapterType, ...]): The + adapter types allowed for this intrinsic, e.g. + ``[AdapterType.ALORA, AdapterType.LORA]``. + available_adapters (dict[str, T]): The available adapters to choose from; + maps ``adapter.qualified_name`` to the adapter object. Returns: - an Adapter if found; else None + T | None: The first matching adapter found, or ``None`` if no match exists. """ adapter = None for adapter_type in intrinsic_adapter_types: @@ -210,30 +261,66 @@ def get_adapter_for_intrinsic( class AdapterMixin(Backend, abc.ABC): - """Mixin class for backends capable of utilizing adapters.""" + """Mixin class for backends capable of utilizing adapters. + + Attributes: + base_model_name (str): The short model name used to identify adapter + variants (e.g. ``"granite-3.3-8b-instruct"`` for + ``"ibm-granite/granite-3.3-8b-instruct"``). + """ @property @abc.abstractmethod def base_model_name(self) -> str: - """Returns the base_model_id of the model used by the backend. For example, `granite-3.3-8b-instruct` for `ibm-granite/granite-3.3-8b-instruct`.""" + """Return the short model name used for adapter variant lookup. + + Returns: + str: The base model name (e.g. ``"granite-3.3-8b-instruct"``). + """ @abc.abstractmethod def add_adapter(self, *args, **kwargs): - """Adds the given adapter to the backend. Must not have been added to a different backend.""" + """Register an adapter with this backend so it can be loaded later. + + The adapter must not already have been added to a different backend. + + Args: + args: Positional arguments forwarded to the concrete implementation. + kwargs: Keyword arguments forwarded to the concrete implementation. + """ @abc.abstractmethod def load_adapter(self, adapter_qualified_name: str): - """Loads the given adapter for the backend. Must have previously been added.""" + """Load a previously registered adapter into the underlying model. + + The adapter must have been registered via ``add_adapter`` before calling + this method. + + Args: + adapter_qualified_name (str): The ``adapter.qualified_name`` of the + adapter to load. + """ @abc.abstractmethod def unload_adapter(self, adapter_qualified_name: str): - """Unloads the given adapter from the backend.""" + """Unload a previously loaded adapter from the underlying model. + + Args: + adapter_qualified_name (str): The ``adapter.qualified_name`` of the + adapter to unload. + """ @abc.abstractmethod def list_adapters(self) -> list[str]: - """Lists the adapters added via add_adapter(). + """Return the qualified names of all adapters currently loaded in this backend. - :returns: list of adapter names that are currently registered with this backend + Returns: + list[str]: Qualified adapter names for all adapters that have been + loaded via ``load_adapter``. + + Raises: + NotImplementedError: If the concrete backend subclass has not + implemented this method. """ raise NotImplementedError( f"Backend type {type(self)} does not implement list_adapters() API call." @@ -241,17 +328,23 @@ def list_adapters(self) -> list[str]: class CustomIntrinsicAdapter(IntrinsicAdapter): - """Special class for users to subclass. + """Special class for users to subclass when creating custom intrinsic adapters. The documentation says that any developer who creates an intrinsic should create a subclass of this class. Creating a subclass of this class appears to be a cosmetic - boilerplate development task that isn't actually to be necessary for any existing - use case. + boilerplate development task that isn't actually necessary for any existing use case. - This class has the same functionality as `IntrinsicsAdapter`, except that its + This class has the same functionality as ``IntrinsicAdapter``, except that its constructor monkey-patches Mellea global variables to enable the backend to load the user's adapter. The code that performs this monkey-patching is marked as a temporary hack. + + Args: + model_id (str): The HuggingFace model ID used for downloading model weights; + expected format is ``"/"``. + intrinsic_name (str | None): Catalog name for the intrinsic; defaults to the + repository name portion of ``model_id`` if not provided. + base_model_name (str): The short name of the base model (NOT its repo ID). """ def __init__( diff --git a/mellea/backends/adapters/catalog.py b/mellea/backends/adapters/catalog.py index 2ff376b2a..e60223493 100644 --- a/mellea/backends/adapters/catalog.py +++ b/mellea/backends/adapters/catalog.py @@ -10,7 +10,12 @@ class AdapterType(enum.Enum): - """Possible types of adapters for a backend.""" + """Possible types of adapters for a backend. + + Attributes: + LORA (str): Standard LoRA adapter; value ``"lora"``. + ALORA (str): Activated LoRA adapter; value ``"alora"``. + """ LORA = "lora" ALORA = "alora" @@ -20,6 +25,16 @@ class IntriniscsCatalogEntry(pydantic.BaseModel): """A single row in the main intrinsics catalog table. We use Pydantic for this dataclass because the rest of Mellea also uses Pydantic. + + Attributes: + name (str): User-visible name of the intrinsic. + internal_name (str | None): Internal name used for adapter loading, or + ``None`` if the same as ``name``. + repo_id (str): HuggingFace repository where adapters for the intrinsic + are located. + adapter_types (tuple[AdapterType, ...]): Adapter types known to be + available for this intrinsic; defaults to + ``(AdapterType.LORA, AdapterType.ALORA)``. """ name: str = pydantic.Field(description="User-visible name of the intrinsic.") @@ -86,10 +101,14 @@ def fetch_intrinsic_metadata(intrinsic_name: str) -> IntriniscsCatalogEntry: """Retrieve information about the adapter that backs an intrinsic. Args: - intrinsic_name: User-visible name of the intrinsic. + intrinsic_name (str): User-visible name of the intrinsic. Returns: - Metadata about the adapter(s) that implement the intrinsic. + IntriniscsCatalogEntry: Metadata about the adapter(s) that implement the + intrinsic. + + Raises: + ValueError: If ``intrinsic_name`` is not a known intrinsic name. """ if intrinsic_name not in _INTRINSICS_CATALOG: raise ValueError( diff --git a/mellea/backends/backend.py b/mellea/backends/backend.py index 79b21c844..3421778be 100644 --- a/mellea/backends/backend.py +++ b/mellea/backends/backend.py @@ -27,6 +27,17 @@ class FormatterBackend(Backend, abc.ABC): The `mellea` library supports these legacy models primarily through prompt engineering surfaced via `FormatterBackends`. A `FormatterBackend` is a backend that uses hand-engineered prompts for rendering generative programming primitives to a model and parsing responses from the model back into `mellea`. By default, a `FormatterBackend` uses jinja2 templates for pretty-printing, and relies on the user's ad-hoc logic for parsing. + + Args: + model_id (str | ModelIdentifier): The model identifier to use for generation. + formatter (ChatFormatter): The formatter used to convert components into prompts. + model_options (dict | None): Default model options; if ``None``, an empty dict + is used. + + Attributes: + model_id (str | ModelIdentifier): The model identifier for this backend. + model_options (dict): The default model options for generation requests. + formatter (ChatFormatter): The formatter used to render components into prompts. """ def __init__( diff --git a/mellea/backends/bedrock.py b/mellea/backends/bedrock.py index e3ae3012b..a340dba94 100644 --- a/mellea/backends/bedrock.py +++ b/mellea/backends/bedrock.py @@ -59,14 +59,22 @@ def create_bedrock_mantle_backend( """Return an OpenAI backend that points to Bedrock mantle for the given model. Args: - model_id: The model to use, either as a ``ModelIdentifier`` (which must have - a ``bedrock_name``) or a raw Bedrock model ID string. - region: AWS region name, or ``None`` to use the default region - (``"us-east-1"``). + model_id (ModelIdentifier | str): The model to use, either as a + ``ModelIdentifier`` (which must have a ``bedrock_name``) or a raw + Bedrock model ID string. + region (str | None): AWS region name, or ``None`` to use the default + region (``"us-east-1"``). Returns: - An ``OpenAIBackend`` configured to call the specified model via AWS Bedrock - Mantle. + OpenAIBackend: An ``OpenAIBackend`` configured to call the specified model + via AWS Bedrock Mantle. + + Raises: + Exception: If ``model_id`` is a ``ModelIdentifier`` with no ``bedrock_name`` + set. + AssertionError: If the ``AWS_BEARER_TOKEN_BEDROCK`` environment variable is + not set. + Exception: If the specified model is not available in the target region. """ model_name = "" match model_id: diff --git a/mellea/backends/cache.py b/mellea/backends/cache.py index 79668cb04..c1c9238fd 100644 --- a/mellea/backends/cache.py +++ b/mellea/backends/cache.py @@ -20,17 +20,37 @@ class Cache(abc.ABC): @abc.abstractmethod def put(self, key: str | int, value: Any) -> None: - """Inserts into the cache. May result in eviction of other cached values.""" + """Insert a value into the cache under the given key. + + May trigger eviction of existing entries if the cache is at capacity. + + Args: + key (str | int): The cache key to store the value under. + value (Any): The value to store. + """ ... @abc.abstractmethod def get(self, key: str | int) -> Any | None: - """Retrieves a value from the cache. Returns `None` if the `id` has no cached value. May impact which cache values are evicted.""" + """Retrieve a value from the cache by key. + + May affect which entries are considered for future eviction (e.g. LRU ordering). + + Args: + key (str | int): The cache key to look up. + + Returns: + Any | None: The cached value, or ``None`` if ``key`` has no cached entry. + """ ... @abc.abstractmethod def current_size(self) -> int: - """Returns the number of things currently in the cache. Mostly useful for debugging.""" + """Return the number of entries currently stored in the cache. + + Returns: + int: Count of items currently held in the cache; useful for debugging. + """ ... @@ -40,6 +60,16 @@ class SimpleLRUCache(Cache): Evicts the least-recently-used entry when capacity is exceeded, optionally invoking an ``on_evict`` callback (e.g. to free GPU memory). Used by local HuggingFace backends to store and reuse KV cache state across requests. + + Args: + capacity (int): Maximum number of items to store in the cache. + on_evict (Callable[[Any], None] | None): Optional callback invoked with the + evicted value whenever an entry is removed to make room for a new one. + + Attributes: + capacity (int): Maximum number of items this cache will hold. + cache (OrderedDict): Internal ordered dict used for LRU tracking. + on_evict (Callable[[Any], None] | None): Eviction callback, or ``None`` if not set. """ def __init__(self, capacity: int, on_evict: Callable[[Any], None] | None = None): @@ -57,11 +87,22 @@ def __init__(self, capacity: int, on_evict: Callable[[Any], None] | None = None) self.on_evict = on_evict def current_size(self) -> int: - """Just return the size of the key set. This isn't necessarily safe.""" + """Return the number of entries currently stored in the cache. + + Returns: + int: Count of items currently held in the cache; useful for debugging. + """ return len(self.cache.keys()) def get(self, key: str | int) -> Any | None: - """Gets a value from the cache.""" + """Retrieve a value from the cache, promoting it to most-recently-used. + + Args: + key (str | int): The cache key to look up. + + Returns: + Any | None: The cached value, or ``None`` if ``key`` is not present. + """ if key not in self.cache: return None else: @@ -71,7 +112,15 @@ def get(self, key: str | int) -> Any | None: return value def put(self, key: str | int, value: Any) -> None: - """Put a value into the cache.""" + """Insert or update a value in the cache. + + If the cache is at capacity and the key is new, the least-recently-used + entry is evicted first, invoking the ``on_evict`` callback if set. + + Args: + key (str | int): The cache key to store the value under. + value (Any): The value to cache. + """ if self.capacity <= 0: return if key in self.cache: diff --git a/mellea/backends/dummy.py b/mellea/backends/dummy.py index e3e12e100..14136b4a9 100644 --- a/mellea/backends/dummy.py +++ b/mellea/backends/dummy.py @@ -12,7 +12,22 @@ class DummyBackend(Backend): - """A backend for smoke testing.""" + """A backend for smoke testing. + + Returns predetermined string responses in sequence, or ``"dummy"`` if no + responses are provided. Intended for unit tests and integration smoke tests + where real model inference is not needed. + + Args: + responses (list[str] | None): Ordered list of strings to return on + successive ``generate_from_context`` calls, or ``None`` to always + return ``"dummy"``. + + Attributes: + responses (list[str] | None): The list of predetermined responses, or + ``None`` if the backend always returns ``"dummy"``. + idx (int): Index of the next response to return from ``responses``. + """ def __init__(self, responses: list[str] | None): """Initializes the dummy backend, optionally with a list of dummy responses. @@ -32,7 +47,28 @@ async def _generate_from_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """See constructor for an exmplanation of how DummyBackends work.""" + """Return the next predetermined response for ``action`` given ``ctx``. + + If ``responses`` is ``None``, always returns the string ``"dummy"``. + Otherwise returns the next item from ``responses`` in order. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Must be ``None``; constrained + decoding is not supported. + model_options (dict | None): Ignored by this backend. + tool_calls (bool): Ignored by this backend. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the predetermined + response and an updated context. + + Raises: + AssertionError: If ``format`` is not ``None``. + Exception: If all responses from ``responses`` have been consumed. + """ assert format is None, "The DummyBackend does not support constrained decoding." if self.responses is None: mot = ModelOutputThunk(value="dummy") diff --git a/mellea/backends/huggingface.py b/mellea/backends/huggingface.py index c7886e7e2..c185ba05d 100644 --- a/mellea/backends/huggingface.py +++ b/mellea/backends/huggingface.py @@ -78,7 +78,28 @@ @dataclasses.dataclass class HFAloraCacheInfo: - """A dataclass for holding some KV cache and associated information.""" + """A dataclass for holding a KV cache and associated generation metadata. + + Used by ``LocalHFBackend`` to store intermediate model state that can be + reused across generation requests via an LRU cache. + + Args: + kv_cache (DynamicCache | None): The HuggingFace ``DynamicCache`` holding + precomputed key/value tensors, or ``None`` if not available. + merged_token_ids (Any): Token IDs corresponding to the cached prefix. + merged_attention (Any): Attention mask for the cached prefix tokens. + q_end (int): Index of the last prompt token in the merged token sequence; + defaults to ``-1``. + scores (Any): Optional logit scores from the generation step; defaults to + ``None``. + + Attributes: + kv_cache (DynamicCache | None): The cached key/value tensors. + merged_token_ids (Any): Token IDs for the cached prefix. + merged_attention (Any): Attention mask for the cached prefix. + q_end (int): End index of the prompt portion in merged token IDs. + scores (Any): Logit scores from generation, or ``None``. + """ kv_cache: DynamicCache | None merged_token_ids: Any @@ -196,6 +217,29 @@ class LocalHFBackend(FormatterBackend, AdapterMixin): This backend is designed for running an HF model for small-scale inference locally on your machine. This backend is NOT designed for inference scaling on CUDA-enabled hardware. + + Args: + model_id (str | ModelIdentifier): Used to load the model and tokenizer via + HuggingFace ``Auto*`` classes. + formatter (ChatFormatter | None): Formatter for rendering components into + prompts. Defaults to ``TemplateFormatter``. + use_caches (bool): If ``False``, KV caching is disabled even if a ``Cache`` + is provided. + cache (Cache | None): Caching strategy; defaults to + ``SimpleLRUCache(0, on_evict=_cleanup_kv_cache)``. + custom_config (TransformersTorchConfig | None): Override for + tokenizer/model/device; if provided, ``model_id`` is not used for loading. + default_to_constraint_checking_alora (bool): If ``False``, aLoRA constraint + checking is deactivated; mainly for benchmarking and debugging. + model_options (dict | None): Default model options for generation requests. + + Attributes: + to_mellea_model_opts_map (dict): Mapping from HF-specific option names to + Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map (dict): Mapping from Mellea sentinel keys to + HF-specific option names. + default_to_constraint_checking_alora (bool): Whether aLoRA constraint checking + is enabled by default. """ _cached_blocks: dict[str, DynamicCache] = dict() @@ -320,7 +364,26 @@ async def _generate_from_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """Generate using the huggingface model.""" + """Generate a completion for ``action`` given ``ctx`` using the HuggingFace model. + + Automatically routes ``Requirement`` and ``Intrinsic`` actions to their + corresponding aLoRA adapters when available. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context (must be a chat context). + format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured/constrained output decoding via llguidance. + model_options (dict | None): Per-call model options that override the + backend's defaults. + tool_calls (bool): If ``True``, expose available tools to the model and + parse tool-call responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ span = start_generate_span( backend=self, action=action, ctx=ctx, format=format, tool_calls=tool_calls ) @@ -983,7 +1046,19 @@ async def _generate_from_context_standard( async def processing( self, mot: ModelOutputThunk, chunk: str | GenerateDecoderOnlyOutput, input_ids ): - """Process the returned chunks or the complete response.""" + """Accumulate decoded text from a streaming chunk or full generation output. + + For streaming responses the chunk is an already-decoded string from + ``AsyncTextIteratorStreamer``; for non-streaming it is a + ``GenerateDecoderOnlyOutput`` that is decoded here. + + Args: + mot (ModelOutputThunk): The output thunk being populated. + chunk (str | GenerateDecoderOnlyOutput): A decoded text chunk (streaming) + or a full HuggingFace generation output object (non-streaming). + input_ids: The prompt token IDs used for decoding; required to slice off + the prompt portion from the generated sequences. + """ if mot._underlying_value is None: mot._underlying_value = "" @@ -1008,7 +1083,23 @@ async def post_processing( seed, input_ids, ): - """Called when generation is done.""" + """Finalize the output thunk after HuggingFace generation completes. + + Stores the KV cache for future reuse, parses tool calls if applicable, + records token usage metrics, emits telemetry, and attaches the generate log. + + Args: + mot (ModelOutputThunk): The output thunk to finalize. + conversation (list[dict]): The chat conversation sent to the model, + used for logging. + _format (type[BaseModelSubclass] | None): The structured output format + class used during generation, if any. + tool_calls (bool): Whether tool calling was enabled for this request. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name. + seed: The random seed used during generation, or ``None``. + input_ids: The prompt token IDs; used to compute token counts and for + KV cache bookkeeping. + """ if mot._meta.get("hf_output", None) is None: if mot._generate_extra is not None: full_output = await mot._generate_extra @@ -1181,7 +1272,22 @@ async def generate_from_raw( model_options: dict | None = None, tool_calls: bool = False, ) -> list[ModelOutputThunk]: - """Generate using the completions api. Gives the input provided to the model without templating.""" + """Generate completions for multiple actions without chat templating. + + Passes formatted prompt strings directly to the HuggingFace model's + ``generate`` method as a batch. Tool calling is not supported. + + Args: + actions (Sequence[Component[C] | CBlock]): Actions to generate completions for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Optional Pydantic model for + structured output decoding via llguidance. + model_options (dict | None): Per-call model options. + tool_calls (bool): Ignored; tool calling is not supported on this endpoint. + + Returns: + list[ModelOutputThunk]: A list of model output thunks, one per action. + """ with instrument_generate_from_raw( backend=self, num_actions=len(actions), format=format, tool_calls=tool_calls ): @@ -1282,13 +1388,26 @@ async def generate_from_raw( # region cache management def cache_get(self, id: str | int) -> HFAloraCacheInfo | None: - """Retrieve from cache.""" + """Retrieve a cached ``HFAloraCacheInfo`` entry by its key. + + Args: + id (str | int): The cache key to look up. + + Returns: + HFAloraCacheInfo | None: The cached entry, or ``None`` if not found. + """ v = self._cache.get(id) assert v is None or type(v) is HFAloraCacheInfo return v def cache_put(self, id: str | int, v: HFAloraCacheInfo): - """Put into cache.""" + """Store an ``HFAloraCacheInfo`` entry in the cache under the given key. + + Args: + id (str | int): The cache key to store the entry under. + v (HFAloraCacheInfo): The cache entry containing KV cache state + and associated generation metadata. + """ self._cache.put(id, v) # endregion @@ -1370,7 +1489,18 @@ def base_model_name(self): return self._hf_model_id.split("/")[1] def add_adapter(self, adapter: LocalHFAdapter): - """Adds the given adapter to the backend. Must not have been added to a different backend.""" + """Register a LoRA/aLoRA adapter with this backend so it can be loaded later. + + Downloads the adapter weights (via ``adapter.get_local_hf_path``) and records + the adapter in the backend's registry. The adapter must not already be + registered with a different backend. + + Args: + adapter (LocalHFAdapter): The adapter to register with this backend. + + Raises: + Exception: If ``adapter`` has already been added to a different backend. + """ if adapter.backend is not None: if adapter.backend is self: FancyLogger.get_logger().warning( @@ -1393,7 +1523,19 @@ def add_adapter(self, adapter: LocalHFAdapter): self._added_adapters[adapter.qualified_name] = adapter def load_adapter(self, adapter_qualified_name: str): - """Loads the given adapter for the backend. Must have previously been added. Do not call when generation requests are happening.""" + """Load a previously registered adapter into the underlying HuggingFace model. + + The adapter must have been registered via ``add_adapter`` first. Do not call + this method while generation requests are in progress. + + Args: + adapter_qualified_name (str): The ``adapter.qualified_name`` of the adapter + to load (i.e. ``"_"``) + + Raises: + ValueError: If no adapter with the given qualified name has been added to + this backend. + """ adapter = self._added_adapters.get(adapter_qualified_name, None) if adapter is None: raise ValueError( @@ -1425,7 +1567,15 @@ def load_adapter(self, adapter_qualified_name: str): self._loaded_adapters[adapter.qualified_name] = adapter def unload_adapter(self, adapter_qualified_name: str): - """Unloads the given adapter from the backend.""" + """Unload a previously loaded adapter from the underlying HuggingFace model. + + If the adapter is not currently loaded, a log message is emitted and the + method returns without error. + + Args: + adapter_qualified_name (str): The ``adapter.qualified_name`` of the adapter + to unload. + """ # Check if the backend knows about this adapter. adapter = self._loaded_adapters.get(adapter_qualified_name, None) if adapter is None: @@ -1440,9 +1590,11 @@ def unload_adapter(self, adapter_qualified_name: str): del self._loaded_adapters[adapter.qualified_name] def list_adapters(self) -> list[str]: - """Lists the adapters added via add_adapter(). + """List the qualified names of all adapters currently loaded in this backend. - :returns: list of adapter names that are currently registered with this backend + Returns: + list[str]: Qualified adapter names (i.e. ``adapter.qualified_name``) for + all adapters that have been loaded via ``load_adapter``. """ return list(self._loaded_adapters.keys()) diff --git a/mellea/backends/litellm.py b/mellea/backends/litellm.py index 4f5bac389..26259e6aa 100644 --- a/mellea/backends/litellm.py +++ b/mellea/backends/litellm.py @@ -52,7 +52,23 @@ class LiteLLMBackend(FormatterBackend): - """A generic LiteLLM compatible backend.""" + """A generic LiteLLM compatible backend. + + Args: + model_id (str): The LiteLLM model identifier string; typically + ``"//"``. + formatter (ChatFormatter | None): Formatter for rendering components. + Defaults to ``TemplateFormatter``. + base_url (str | None): Base URL for the LLM API endpoint; defaults to + the Ollama local endpoint. + model_options (dict | None): Default model options for generation requests. + + Attributes: + to_mellea_model_opts_map (dict): Mapping from backend-specific option names to + Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map (dict): Mapping from Mellea ``ModelOption`` sentinel + keys to backend-specific option names. + """ def __init__( self, @@ -127,7 +143,26 @@ async def _generate_from_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """See `generate_from_chat_context`.""" + """Generate a completion for ``action`` given ``ctx`` via the LiteLLM chat API. + + Delegates to ``_generate_from_chat_context_standard``. Only chat contexts are + supported; raises ``NotImplementedError`` otherwise. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context (must be a chat context). + format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured/constrained output decoding. + model_options (dict | None): Per-call model options that override the + backend's defaults. + tool_calls (bool): If ``True``, expose available tools to the model and + parse tool-call responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ assert ctx.is_chat_context, NotImplementedError( "The Openai backend only supports chat-like contexts." ) @@ -367,9 +402,16 @@ async def processing( mot: ModelOutputThunk, chunk: litellm.ModelResponse | litellm.ModelResponseStream, # type: ignore ): - """Called during generation to add information from a single ModelResponse or a chunk / ModelResponseStream to the ModelOutputThunk. + """Accumulate content and thinking tokens from a single LiteLLM response chunk. + + Called during generation for each ``ModelResponse`` (non-streaming) or + ``ModelResponseStream`` chunk (streaming). Tool call parsing is deferred to + ``post_processing``. - For LiteLLM, tool call parsing is handled in the post processing step. + Args: + mot (ModelOutputThunk): The output thunk being populated. + chunk (litellm.ModelResponse | litellm.ModelResponseStream): A single + response object or streaming chunk from LiteLLM. """ if mot._thinking is None: mot._thinking = "" @@ -430,7 +472,21 @@ async def post_processing( thinking, _format, ): - """Called when generation is done.""" + """Finalize the model output thunk after LiteLLM generation completes. + + Reconstructs a merged chat response from streaming chunks if applicable, + extracts tool call requests, records token usage metrics, emits telemetry, + and attaches the generate log to the output thunk. + + Args: + mot (ModelOutputThunk): The output thunk to finalize. + conversation (list[dict]): The chat conversation sent to the model, + used for logging. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name. + thinking: The thinking/reasoning effort level passed to the model, or + ``None`` if reasoning mode was not enabled. + _format: The structured output format class used during generation, if any. + """ # Reconstruct the chat_response from chunks if streamed. streamed_chunks = mot._meta.get("litellm_chat_response_streamed", None) if streamed_chunks is not None: @@ -578,7 +634,22 @@ async def generate_from_raw( model_options: dict | None = None, tool_calls: bool = False, ) -> list[ModelOutputThunk]: - """Generate using the completions api. Gives the input provided to the model without templating.""" + """Generate completions for multiple actions without chat templating via LiteLLM. + + Passes formatted prompt strings directly to LiteLLM's text completion endpoint. + Tool calling is not supported on this endpoint. + + Args: + actions (Sequence[Component[C] | CBlock]): Actions to generate completions for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Optional Pydantic model for + structured output; passed as ``guided_json`` in the request body. + model_options (dict | None): Per-call model options. + tool_calls (bool): Ignored; tool calling is not supported on this endpoint. + + Returns: + list[ModelOutputThunk]: A list of model output thunks, one per action. + """ with instrument_generate_from_raw( backend=self, num_actions=len(actions), format=format, tool_calls=tool_calls ): diff --git a/mellea/backends/model_ids.py b/mellea/backends/model_ids.py index d13bcaeb3..7a85bc408 100644 --- a/mellea/backends/model_ids.py +++ b/mellea/backends/model_ids.py @@ -17,6 +17,24 @@ class ModelIdentifier: Using model strings is messy: 1. Different platforms use variations on model id strings. 2. Using raw strings is annoying because: no autocomplete, typos, hallucinated names, mismatched model and tokenizer names, etc. + + Args: + hf_model_name (str | None): HuggingFace Hub model repository ID (e.g. ``"ibm-granite/granite-3.3-8b-instruct"``). + ollama_name (str | None): Ollama model tag (e.g. ``"granite3.3:8b"``). + watsonx_name (str | None): WatsonX AI model ID (e.g. ``"ibm/granite-3-2b-instruct"``). + mlx_name (str | None): MLX model identifier for Apple Silicon inference. + openai_name (str | None): OpenAI API model name (e.g. ``"gpt-5.1"``). + bedrock_name (str | None): AWS Bedrock model ID (e.g. ``"openai.gpt-oss-20b"``). + hf_tokenizer_name (str | None): HuggingFace tokenizer ID; defaults to ``hf_model_name`` if ``None``. + + Attributes: + hf_model_name (str | None): HuggingFace Hub model repository ID. + ollama_name (str | None): Ollama model tag. + watsonx_name (str | None): WatsonX AI model ID. + mlx_name (str | None): MLX model identifier. + openai_name (str | None): OpenAI API model name. + bedrock_name (str | None): AWS Bedrock model ID. + hf_tokenizer_name (str | None): HuggingFace tokenizer ID; same as ``hf_model_name`` when ``None``. """ hf_model_name: str | None = None diff --git a/mellea/backends/model_options.py b/mellea/backends/model_options.py index 2df6f510f..428dcab00 100644 --- a/mellea/backends/model_options.py +++ b/mellea/backends/model_options.py @@ -19,6 +19,16 @@ class ModelOption: ModelOption.SYSTEM_PROMPT : "You are a helpful assistant" } ``` + + Attributes: + TOOLS (str): Sentinel key for a list or dict of tools to expose for tool calling. + MAX_NEW_TOKENS (str): Sentinel key for the maximum number of new tokens to generate. + SYSTEM_PROMPT (str): Sentinel key for the system prompt string. + TEMPERATURE (str): Key for the sampling temperature (passed through to the backend). + CONTEXT_WINDOW (str): Sentinel key for the context window size. + THINKING (str): Sentinel key for enabling/configuring reasoning/thinking mode. + SEED (str): Sentinel key for the random seed for reproducible generation. + STREAM (str): Sentinel key for enabling streaming responses. """ TOOLS = "@@@tools@@@" @@ -34,7 +44,9 @@ class ModelOption: @staticmethod def replace_keys(options: dict, from_to: dict[str, str]) -> dict[str, Any]: - """Returns a new dict with the keys in `options` replaced with the corresponding value for that key in `from_to`. + """Return a new dict with selected keys in ``options`` renamed according to ``from_to``. + + Returns a new dict with the keys in `options` replaced with the corresponding value for that key in `from_to`. * Any key with value == None is treated the same as the key missing. @@ -55,6 +67,13 @@ def replace_keys(options: dict, from_to: dict[str, str]) -> dict[str, Any]: * Notice that "M1" keeps the original value "m1", rather than "v1". * Notice that both "k1" and "k2" are absent in the output. + + Args: + options (dict): The source dictionary whose keys may be renamed. + from_to (dict[str, str]): Mapping of old key names to new key names. + + Returns: + dict[str, Any]: A new dictionary with the specified keys renamed. """ new_options = {} @@ -93,7 +112,17 @@ def replace_keys(options: dict, from_to: dict[str, str]) -> dict[str, Any]: @staticmethod def remove_special_keys(model_options: dict[str, Any]) -> dict[str, Any]: - """Removes all sentiel-valued keys (i.e., those that start with @@@).""" + """Return a copy of ``model_options`` with all sentinel-valued keys removed. + + Sentinel keys are those whose names start with ``@@@`` (e.g. ``ModelOption.TOOLS``). + These are Mellea-internal keys that must not be forwarded to backend APIs. + + Args: + model_options (dict[str, Any]): A model options dictionary that may contain sentinel keys. + + Returns: + dict[str, Any]: A new dictionary with all ``@@@``-prefixed keys omitted. + """ new_options = {} for k, v in model_options.items(): if not k.startswith("@@@"): @@ -104,7 +133,19 @@ def remove_special_keys(model_options: dict[str, Any]) -> dict[str, Any]: def merge_model_options( persistent_opts: dict[str, Any], overwrite_opts: dict[str, Any] | None ) -> dict[str, Any]: - """Creates a new dict that contains all keys and values from persistent opts and overwrite opts. If there are duplicate keys, overwrite opts key value pairs will be used.""" + """Merge two model-options dicts, with ``overwrite_opts`` taking precedence on conflicts. + + Creates a new dict that contains all keys and values from persistent opts and overwrite opts. + If there are duplicate keys, overwrite opts key value pairs will be used. + + Args: + persistent_opts (dict[str, Any]): Base model options (lower precedence). + overwrite_opts (dict[str, Any] | None): Per-call model options that override + ``persistent_opts`` on key conflicts; ``None`` is treated as empty. + + Returns: + dict[str, Any]: A new merged dictionary. + """ new_options = {} for k, v in persistent_opts.items(): diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index afe80ec84..a5edd6a0a 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -39,7 +39,24 @@ class OllamaModelBackend(FormatterBackend): - """A model that uses the Ollama Python SDK for local inference.""" + """A model that uses the Ollama Python SDK for local inference. + + Args: + model_id (str | ModelIdentifier): Ollama model ID. If a + ``ModelIdentifier`` is passed, its ``ollama_name`` attribute must + be set. + formatter (ChatFormatter | None): Formatter for rendering components. + Defaults to ``TemplateFormatter``. + base_url (str | None): Ollama server endpoint; defaults to + ``env(OLLAMA_HOST)`` or ``http://localhost:11434``. + model_options (dict | None): Default model options for generation requests. + + Attributes: + to_mellea_model_opts_map (dict): Mapping from Ollama-specific option names + to Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map (dict): Mapping from Mellea ``ModelOption`` + sentinel keys to Ollama-specific option names. + """ def __init__( self, @@ -259,7 +276,25 @@ async def _generate_from_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """See `generate_from_chat_context`.""" + """Generate a completion for ``action`` given ``ctx`` via the Ollama chat API. + + Delegates to ``generate_from_chat_context``. Only chat contexts are supported. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context (must be a chat context). + format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured/constrained output decoding. + model_options (dict | None): Per-call model options that override the + backend's defaults. + tool_calls (bool): If ``True``, expose available tools to the model and + parse tool-call responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ from ..telemetry.backend_instrumentation import start_generate_span # Start span without auto-closing (will be closed in post_processing) @@ -291,12 +326,23 @@ async def generate_from_chat_context( model_options: dict | None = None, tool_calls: bool = False, ) -> ModelOutputThunk[C]: - """Generates a ModelOutputThunk. The final value for this object can be awaited. + """Generate a new completion from the provided context using this backend's formatter. + + Treats the ``Context`` as a chat history and uses the ``ollama.Client.chat()`` + interface to generate a completion. Returns a thunk that lazily resolves + the model output. - The new completion is generated from the provided Context using this backend's `Formatter`. + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context (must be a chat context). + _format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured output decoding. + model_options (dict | None): Per-call model options. + tool_calls (bool): If ``True``, expose available tools and parse responses. - This implementation treats the `Context` as a chat history, and uses the `ollama.Client.chat()` interface to generate a completion. - This will not always work, because sometimes we want to use non-chat models. + Returns: + ModelOutputThunk[C]: A thunk holding the (lazy) model output. Raises: RuntimeError: If not called from a thread with a running event loop. @@ -429,7 +475,22 @@ async def generate_from_raw( model_options: dict | None = None, tool_calls: bool = False, ) -> list[ModelOutputThunk]: - """Generate using the generate api. Gives the input provided to the model without templating.""" + """Generate completions for multiple actions without chat templating via Ollama. + + Passes formatted prompt strings directly to Ollama's generate endpoint. + Requests are submitted concurrently to make use of Ollama's concurrency support. + + Args: + actions (Sequence[Component[C] | CBlock]): Actions to generate completions for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Optional Pydantic model for + structured output decoding. + model_options (dict | None): Per-call model options. + tool_calls (bool): Ignored; tool calling is not supported on this endpoint. + + Returns: + list[ModelOutputThunk]: A list of model output thunks, one per action. + """ if len(actions) > 1: FancyLogger.get_logger().info( "Ollama doesn't support batching; will attempt to process concurrently." @@ -554,7 +615,18 @@ async def processing( chunk: ollama.ChatResponse, tools: dict[str, AbstractMelleaTool], ): - """Called during generation to add information from a single ChatResponse to the ModelOutputThunk.""" + """Accumulate text and tool calls from a single Ollama ChatResponse chunk. + + Called for each streaming or non-streaming ``ollama.ChatResponse``. Also + extracts tool call requests inline and merges the chunk into the running + aggregated response stored in ``mot._meta["chat_response"]``. + + Args: + mot (ModelOutputThunk): The output thunk being populated. + chunk (ollama.ChatResponse): A single chat response object from Ollama. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name, + used for extracting tool call requests from the response. + """ if mot._thinking is None: mot._thinking = "" thinking_chunk = chunk.message.thinking @@ -587,7 +659,18 @@ async def post_processing( tools: dict[str, AbstractMelleaTool], _format, ): - """Called when generation is done.""" + """Finalize the output thunk after Ollama generation completes. + + Attaches the generate log, records token usage metrics, emits telemetry, + and cleans up the span reference. + + Args: + mot (ModelOutputThunk): The output thunk to finalize. + conversation (list[dict]): The chat conversation sent to the model, + used for logging. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name. + _format: The structured output format class used during generation, if any. + """ assert mot._action is not None, ( "ModelOutputThunks should have their action assigned during generation" ) diff --git a/mellea/backends/openai.py b/mellea/backends/openai.py index faadfc454..9a3a325c4 100644 --- a/mellea/backends/openai.py +++ b/mellea/backends/openai.py @@ -65,7 +65,33 @@ class OpenAIBackend(FormatterBackend): - """A generic OpenAI compatible backend.""" + """A generic OpenAI compatible backend. + + Args: + model_id (str | ModelIdentifier): OpenAI-compatible model identifier. + Defaults to ``model_ids.OPENAI_GPT_5_1``. + formatter (ChatFormatter | None): Formatter for rendering components. + Defaults to ``TemplateFormatter``. + base_url (str | None): Base URL for the API endpoint; defaults to the + standard OpenAI endpoint if not set. + model_options (dict | None): Default model options for generation requests. + default_to_constraint_checking_alora (bool): If ``False``, deactivates aLoRA + constraint checking; primarily for benchmarking and debugging. + api_key (str | None): API key; falls back to ``OPENAI_API_KEY`` env var. + kwargs: Additional keyword arguments forwarded to the OpenAI client. + + Attributes: + to_mellea_model_opts_map_chats (dict): Mapping from chat-endpoint option names + to Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map_chats (dict): Mapping from Mellea sentinel keys to + chat-endpoint option names. + to_mellea_model_opts_map_completions (dict): Mapping from completions-endpoint + option names to Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map_completions (dict): Mapping from Mellea sentinel keys + to completions-endpoint option names. + default_to_constraint_checking_alora (bool): Whether aLoRA constraint checking + is enabled by default. + """ def __init__( self, @@ -206,15 +232,29 @@ def _async_client(self) -> openai.AsyncOpenAI: @staticmethod def filter_openai_client_kwargs(**kwargs) -> dict: - """Filter kwargs to only include valid OpenAI client parameters.""" + """Filter kwargs to only include valid OpenAI client constructor parameters. + + Args: + kwargs: Arbitrary keyword arguments to filter. + + Returns: + dict: A dict containing only keys accepted by ``openai.OpenAI.__init__``. + """ openai_params = set(inspect.signature(openai.OpenAI.__init__).parameters.keys()) # type: ignore openai_params.discard("self") # Remove 'self' parameter return {k: v for k, v in kwargs.items() if k in openai_params} def filter_chat_completions_kwargs(self, model_options: dict) -> dict: - """Filter kwargs to only include valid OpenAI chat.completions.create parameters. + """Filter model options to only include valid OpenAI chat completions parameters. - https://platform.openai.com/docs/api-reference/chat/create + See https://platform.openai.com/docs/api-reference/chat/create for the full + list of accepted parameters. + + Args: + model_options (dict): Model options dict that may contain non-chat keys. + + Returns: + dict: A dict containing only keys accepted by ``chat.completions.create``. """ from openai.resources.chat.completions import Completions @@ -223,9 +263,16 @@ def filter_chat_completions_kwargs(self, model_options: dict) -> dict: return {k: v for k, v in model_options.items() if k in chat_params} def filter_completions_kwargs(self, model_options: dict) -> dict: - """Filter kwargs to only include valid OpenAI completions.create parameters. + """Filter model options to only include valid OpenAI completions parameters. + + See https://platform.openai.com/docs/api-reference/completions for the full + list of accepted parameters. - https://platform.openai.com/docs/api-reference/completions + Args: + model_options (dict): Model options dict that may contain non-completions keys. + + Returns: + dict: A dict containing only keys accepted by ``completions.create``. """ from openai.resources.completions import Completions @@ -303,7 +350,25 @@ async def _generate_from_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """See `generate_from_chat_context`.""" + """Generate a completion for ``action`` given ``ctx`` via the OpenAI chat API. + + Delegates to ``generate_from_chat_context``. Only chat contexts are supported. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context (must be a chat context). + format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured/constrained output decoding. + model_options (dict | None): Per-call model options that override the + backend's defaults. + tool_calls (bool): If ``True``, expose available tools to the model and + parse tool-call responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ from ..telemetry.backend_instrumentation import start_generate_span assert ctx.is_chat_context, NotImplementedError( @@ -342,7 +407,24 @@ async def generate_from_chat_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """Generates a new completion from the provided Context using this backend's `Formatter`.""" + """Generate a new completion from the provided Context using this backend's ``Formatter``. + + Formats the context and action into OpenAI-compatible chat messages, submits the + request asynchronously, and returns a thunk that lazily resolves the output. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context. + _format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured output decoding. + model_options (dict | None): Per-call model options. + tool_calls (bool): If ``True``, expose available tools and parse responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ await self.do_generate_walk(action) mot = await self._generate_from_chat_context_standard( @@ -508,9 +590,15 @@ async def _generate_from_chat_context_standard( async def processing( self, mot: ModelOutputThunk, chunk: ChatCompletion | ChatCompletionChunk ): - """Called during generation to add information from a single ChatCompletion or ChatCompletionChunk to the ModelOutputThunk. + """Accumulate content from a single OpenAI response object into the output thunk. + + Called for each ``ChatCompletion`` (non-streaming) or ``ChatCompletionChunk`` + (streaming). Tool call parsing is deferred to ``post_processing``. - For OpenAI, tool call parsing is handled in the post processing step. + Args: + mot (ModelOutputThunk): The output thunk being populated. + chunk (ChatCompletion | ChatCompletionChunk): A single response object or + streaming delta from the OpenAI API. """ if mot._thinking is None: mot._thinking = "" @@ -568,7 +656,22 @@ async def post_processing( seed, _format, ): - """Called when generation is done.""" + """Finalize the output thunk after OpenAI generation completes. + + Reconstructs a merged chat response from streaming chunks if applicable, + extracts any tool call requests, records token usage metrics, emits telemetry, + and attaches the generate log. + + Args: + mot (ModelOutputThunk): The output thunk to finalize. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name. + conversation (list[dict]): The chat conversation sent to the model, + used for logging. + thinking: The reasoning effort level passed to the model, or ``None`` + if reasoning mode was not enabled. + seed: The random seed used during generation, or ``None``. + _format: The structured output format class used during generation, if any. + """ # Reconstruct the chat_response from chunks if streamed. streamed_chunks = mot._meta.get("oai_chat_response_streamed", None) if streamed_chunks is not None: @@ -692,7 +795,26 @@ async def generate_from_raw( model_options: dict | None = None, tool_calls: bool = False, ) -> list[ModelOutputThunk]: - """Generate using the completions api. Gives the input provided to the model without templating.""" + """Generate completions for multiple actions without chat templating via the OpenAI completions API. + + Passes formatted prompt strings directly to the completions endpoint. + Tool calling is not supported on this endpoint. + + Args: + actions (Sequence[Component[C] | CBlock]): Actions to generate completions for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Optional Pydantic model for + structured output; passed as a guided-decoding parameter. + model_options (dict | None): Per-call model options. + tool_calls (bool): Ignored; tool calling is not supported on this endpoint. + + Returns: + list[ModelOutputThunk]: A list of model output thunks, one per action. + + Raises: + openai.BadRequestError: If the request is invalid (e.g. when targeting an + Ollama server that does not support batched completion requests). + """ await self.do_generate_walks(list(actions)) extra_body = {} diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 66bb93e6c..609957d7e 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -24,7 +24,21 @@ class MelleaTool(AbstractMelleaTool): - """Tool class to represent a tool.""" + """Tool class to represent a callable tool with an OpenAI-compatible JSON schema. + + Wraps a Python callable alongside its JSON schema representation so it can be + registered with backends that support tool calling (OpenAI, Ollama, HuggingFace, etc.). + + Args: + name (str): The tool name used for identification and lookup. + tool_call (Callable): The underlying Python callable to invoke when the tool is run. + as_json_tool (dict[str, Any]): The OpenAI-compatible JSON schema dict describing + the tool's parameters. + + Attributes: + name (str): The registered name of this tool. + as_json_tool (dict[str, Any]): OpenAI-compatible JSON schema dict for this tool. + """ # Tool is what we pass as a model option / as input # Our ModelToolCall is the class that has a reference to the tool and actually calls with arguments @@ -42,7 +56,15 @@ def __init__( self._call_tool = tool_call def run(self, *args, **kwargs) -> Any: - """Run the tool with the given arguments.""" + """Run the tool with the given arguments. + + Args: + args: Positional arguments forwarded to the underlying callable. + kwargs: Keyword arguments forwarded to the underlying callable. + + Returns: + Any: The return value of the underlying callable. + """ return self._call_tool(*args, **kwargs) @property @@ -52,7 +74,18 @@ def as_json_tool(self) -> dict[str, Any]: @classmethod def from_langchain(cls, tool: Any): - """Create a Tool from a langchain tool object.""" + """Create a MelleaTool from a LangChain tool object. + + Args: + tool (Any): A ``langchain_core.tools.BaseTool`` instance to wrap. + + Returns: + MelleaTool: A Mellea tool wrapping the LangChain tool. + + Raises: + ImportError: If ``langchain-core`` is not installed. + ValueError: If ``tool`` is not a ``BaseTool`` instance. + """ try: from langchain_core.tools import BaseTool # type: ignore[import-not-found] from langchain_core.utils.function_calling import ( # type: ignore[import-not-found] @@ -143,7 +176,18 @@ def tool_call(*args, **kwargs): @classmethod def from_callable(cls, func: Callable, name: str | None = None): - """Create a Tool from a callable.""" + """Create a MelleaTool from a plain Python callable. + + Introspects the callable's signature and docstring to build an + OpenAI-compatible JSON schema automatically. + + Args: + func (Callable): The Python callable to wrap as a tool. + name (str | None): Optional name override; defaults to ``func.__name__``. + + Returns: + MelleaTool: A Mellea tool wrapping the callable. + """ # Use the function name if the name is '' or None. tool_name = name or func.__name__ as_json = convert_function_to_ollama_tool(func, tool_name).model_dump( @@ -666,7 +710,15 @@ def __contains__(self, key: str) -> bool: return False def get(self, key: str, default: Any = None) -> Any: - """Get. + """Return the value of a field by name, or a default if the field does not exist. + + Args: + key (str): The field name to look up on the model. + default (Any): Value to return when ``key`` is not a field on the model. + Defaults to ``None``. + + Returns: + Any: The field value if the attribute exists, otherwise ``default``. >>> msg = Message(role='user') >>> msg.get('role') @@ -685,18 +737,42 @@ def get(self, key: str, default: Any = None) -> Any: # https://github.com/ollama/ollama-python/blob/60e7b2f9ce710eeb57ef2986c46ea612ae7516af/ollama/_types.py#L337-L363 class OllamaTool(SubscriptableBaseModel): - """Class imported from Ollama.""" + """Pydantic model for an Ollama-compatible tool schema, imported from the Ollama Python SDK. + + Represents the JSON structure that Ollama (and OpenAI-compatible endpoints) expect + when a tool is passed to the chat API. Mellea builds these objects internally via + ``convert_function_to_ollama_tool`` and never exposes them to end users directly. + + Attributes: + type (str | None): Tool type; always ``"function"`` for function-calling tools. + function (Function | None): Nested object containing the function name, + description, and parameters schema. + """ type: str | None = "function" class Function(SubscriptableBaseModel): - """Class imported from Ollama.""" + """Pydantic model for the ``function`` field of an Ollama tool schema, imported from the Ollama Python SDK. + + Attributes: + name (str | None): The name of the function being described. + description (str | None): Human-readable description of what the function does. + parameters (Parameters | None): Schema describing the function's parameters. + """ name: str | None = None description: str | None = None class Parameters(SubscriptableBaseModel): - """Class imported from Ollama.""" + """Pydantic model for the ``parameters`` field of an Ollama function schema, imported from the Ollama Python SDK. + + Attributes: + type (Literal["object"] | None): Always ``"object"`` for function parameters. + defs (Any | None): JSON Schema ``$defs`` for referenced sub-schemas. + items (Any | None): Array item schema, if applicable. + required (Sequence[str] | None): List of required parameter names. + properties (Mapping[str, Property] | None): Parameter property definitions. + """ model_config = ConfigDict(populate_by_name=True) type: Literal["object"] | None = "object" @@ -705,7 +781,14 @@ class Parameters(SubscriptableBaseModel): required: Sequence[str] | None = None class Property(SubscriptableBaseModel): - """Class imported from Ollama.""" + """Pydantic model for a single parameter property in an Ollama tool schema, imported from the Ollama Python SDK. + + Attributes: + type (str | Sequence[str] | None): JSON Schema type string or list of type strings. + items (Any | None): Schema for array element types, if applicable. + description (str | None): Human-readable description of this parameter. + enum (Sequence[Any] | None): Allowed values for this parameter, if constrained. + """ model_config = ConfigDict(arbitrary_types_allowed=True) diff --git a/mellea/backends/vllm.py b/mellea/backends/vllm.py index 9ceedd651..55e986aa8 100644 --- a/mellea/backends/vllm.py +++ b/mellea/backends/vllm.py @@ -69,6 +69,20 @@ class LocalVLLMBackend(FormatterBackend): Note: vLLM defaults to ~16 tokens. Always set ModelOption.MAX_NEW_TOKENS explicitly (100-1000+). Structured output needs 200-500+ tokens. + + Args: + model_id (str | ModelIdentifier): HuggingFace model ID used to load model weights via vLLM. + formatter (ChatFormatter | None): Formatter for rendering components into prompts. + Defaults to a ``TemplateFormatter`` for the given ``model_id``. + model_options (dict | None): Default model options for generation requests. + + Attributes: + to_mellea_model_opts_map (dict): Mapping from backend-specific option names to + Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map (dict): Mapping from Mellea ``ModelOption`` sentinel + keys to backend-specific option names. + engine_args (dict): vLLM engine arguments used at instantiation; retained so the + engine can be restarted when the event loop changes. """ def __init__( @@ -243,7 +257,25 @@ async def _generate_from_context( generate_logs: list[GenerateLog] | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """Generate using the huggingface model.""" + """Generate a completion for ``action`` given the current ``ctx`` using the vLLM engine. + + Args: + action (Component[C] | CBlock): The component or content block to generate a + completion for. + ctx (Context): The current generation context (must be a chat context). + format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured/constrained output decoding. + model_options (dict | None): Per-call model options that override the + backend's defaults. + generate_logs (list[GenerateLog] | None): Optional list to which a + ``GenerateLog`` entry will be appended. + tool_calls (bool): If ``True``, expose available tools to the model and + parse tool-call responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ await self.do_generate_walk(action) # Upsert model options. @@ -365,7 +397,15 @@ async def _generate_from_context_standard( raise Exception("Does not yet support non-chat contexts.") async def processing(self, mot: ModelOutputThunk, chunk: vllm.RequestOutput): - """Process the returned chunks or the complete response.""" + """Accumulate text from a single vLLM output chunk into the model output thunk. + + Called during streaming or final generation to add each incremental result to + ``mot._underlying_value``. + + Args: + mot (ModelOutputThunk): The output thunk being populated. + chunk (vllm.RequestOutput): A single output from the vLLM generate stream. + """ if mot._underlying_value is None: mot._underlying_value = "" mot._underlying_value += chunk.outputs[0].text @@ -379,7 +419,21 @@ async def post_processing( tools: dict[str, AbstractMelleaTool], seed, ): - """Called when generation is done.""" + """Finalize the model output thunk after generation completes. + + Parses any tool calls from the raw output, attaches the generate log, and + records metadata needed for telemetry. + + Args: + mot (ModelOutputThunk): The output thunk to finalize. + conversation (list[dict]): The chat conversation sent to the model, + used for logging. + _format (type[BaseModelSubclass] | None): The structured output format + class used during generation, if any. + tool_calls (bool): Whether tool calling was enabled for this request. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name. + seed: The random seed used during generation, or ``None``. + """ # The ModelOutputThunk must be computed by this point. assert mot.value is not None @@ -443,7 +497,22 @@ async def generate_from_raw( model_options: dict | None = None, tool_calls: bool = False, ) -> list[ModelOutputThunk]: - """Generate using the completions api. Gives the input provided to the model without templating.""" + """Generate completions for multiple actions without chat templating. + + Passes the formatted prompt strings directly to vLLM's completion endpoint. + Tool calling is not supported by this method. + + Args: + actions (Sequence[Component[C] | CBlock]): Actions to generate completions for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Optional Pydantic model for + structured output decoding. + model_options (dict | None): Per-call model options. + tool_calls (bool): Ignored; tool calling is not supported on this endpoint. + + Returns: + list[ModelOutputThunk]: A list of model output thunks, one per action. + """ from ..telemetry.backend_instrumentation import instrument_generate_from_raw with instrument_generate_from_raw( diff --git a/mellea/backends/watsonx.py b/mellea/backends/watsonx.py index d6ca943e9..add1a063e 100644 --- a/mellea/backends/watsonx.py +++ b/mellea/backends/watsonx.py @@ -55,7 +55,31 @@ class WatsonxAIBackend(FormatterBackend): - """A generic backend class for watsonx SDK.""" + """A generic backend class for watsonx SDK. + + Args: + model_id (str | ModelIdentifier): WatsonX model identifier. Defaults to + ``model_ids.IBM_GRANITE_4_HYBRID_SMALL``. + formatter (ChatFormatter | None): Formatter for rendering components. + Defaults to ``TemplateFormatter``. + base_url (str | None): URL for the WatsonX ML deployment endpoint; + defaults to the ``WATSONX_URL`` environment variable. + model_options (dict | None): Default model options for generation requests. + api_key (str | None): WatsonX API key; defaults to the + ``WATSONX_API_KEY`` environment variable. + project_id (str | None): WatsonX project ID; defaults to the + ``WATSONX_PROJECT_ID`` environment variable. + + Attributes: + to_mellea_model_opts_map_chats (dict): Mapping from chat-endpoint option names + to Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map_chats (dict): Mapping from Mellea sentinel keys to + chat-endpoint option names. + to_mellea_model_opts_map_completions (dict): Mapping from completions-endpoint + option names to Mellea ``ModelOption`` sentinel keys. + from_mellea_model_opts_map_completions (dict): Mapping from Mellea sentinel + keys to completions-endpoint option names. + """ def __init__( self, @@ -177,7 +201,14 @@ def _get_watsonx_model_id(self) -> str: return watsonx_model_id def filter_chat_completions_kwargs(self, model_options: dict) -> dict: - """Filter kwargs to only include valid watsonx chat.completions.create parameters.""" + """Filter kwargs to only include valid watsonx chat.completions.create parameters. + + Args: + model_options (dict): Model options dict that may contain non-chat keys. + + Returns: + dict: A dict containing only keys accepted by the WatsonX chat endpoint. + """ # TextChatParameters.get_sample_params().keys() can't be completely trusted. It doesn't always contain all # all of the accepted keys. In version 1.3.39, max_tokens was removed even though it's still accepted. # It's a dataclass so use the fields function to get the names. @@ -251,7 +282,26 @@ async def _generate_from_context( model_options: dict | None = None, tool_calls: bool = False, ) -> tuple[ModelOutputThunk[C], Context]: - """See `generate_from_chat_context`.""" + """Generate a completion for ``action`` given ``ctx`` via the WatsonX chat API. + + Delegates to ``generate_from_chat_context``. Only chat contexts are + supported; raises ``NotImplementedError`` otherwise. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context (must be a chat context). + format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured/constrained output decoding. + model_options (dict | None): Per-call model options that override the + backend's defaults. + tool_calls (bool): If ``True``, expose available tools to the model and + parse tool-call responses. + + Returns: + tuple[ModelOutputThunk[C], Context]: A thunk holding the (lazy) model output + and an updated context that includes ``action`` and the new output. + """ assert ctx.is_chat_context, NotImplementedError( "The watsonx.ai backend only supports chat-like contexts." ) @@ -282,7 +332,28 @@ async def generate_from_chat_context( model_options: dict | None = None, tool_calls: bool = False, ) -> ModelOutputThunk[C]: - """Generates a new completion from the provided Context using this backend's `Formatter`.""" + """Generate a new completion from the provided context using this backend's formatter. + + Formats the context and action into WatsonX-compatible chat messages, submits + the request asynchronously, and returns a thunk that lazily resolves the output. + + Args: + action (Component[C] | CBlock): The component or content block to generate + a completion for. + ctx (Context): The current generation context. + _format (type[BaseModelSubclass] | None): Optional Pydantic model class for + structured output decoding. + model_options (dict | None): Per-call model options. + tool_calls (bool): If ``True``, expose available tools and parse responses. + + Returns: + ModelOutputThunk[C]: A thunk holding the (lazy) model output. + + Raises: + Exception: If ``action`` is an ``ALoraRequirement``, which is not + supported by this backend. + RuntimeError: If not called from a thread with a running event loop. + """ await self.do_generate_walk(action) model_opts = self._simplify_and_merge( @@ -402,9 +473,15 @@ async def generate_from_chat_context( return output async def processing(self, mot: ModelOutputThunk, chunk: dict): - """Called during generation to add information from a single ChatCompletion or ChatCompletionChunk to the ModelOutputThunk. + """Accumulate content from a single WatsonX response dict into the output thunk. + + Called for each non-streaming chat dict (with a ``"message"`` key) or + streaming delta dict (with a ``"delta"`` key). Tool call parsing is + handled in the post-processing step. - For OpenAI-like APIs, tool call parsing is handled in the post processing step. + Args: + mot (ModelOutputThunk): The output thunk being populated. + chunk (dict): A single response dict or streaming delta from the WatsonX API. """ if mot._thinking is None: mot._thinking = "" @@ -455,7 +532,20 @@ async def post_processing( seed, _format, ): - """Called when generation is done.""" + """Finalize the output thunk after WatsonX generation completes. + + Reconstructs a merged chat response from streaming chunks if applicable, + extracts any tool call requests, records token usage metrics, emits telemetry, + and attaches the generate log. + + Args: + mot (ModelOutputThunk): The output thunk to finalize. + conversation (list[dict]): The chat conversation sent to the model, + used for logging. + tools (dict[str, AbstractMelleaTool]): Available tools, keyed by name. + seed: The random seed used during generation, or ``None``. + _format: The structured output format class used during generation, if any. + """ # Reconstruct the chat_response from chunks if streamed. streamed_chunks = mot._meta.get("oai_chat_response_streamed", None) if streamed_chunks is not None: @@ -582,7 +672,21 @@ async def generate_from_raw( model_options: dict | None = None, tool_calls: bool = False, ) -> list[ModelOutputThunk]: - """Generates a completion text. Gives the input provided to the model without templating.""" + """Generate completions for multiple actions without chat templating via WatsonX. + + Passes formatted prompt strings directly to WatsonX's generate endpoint. + The ``format`` parameter is not supported and will be ignored with a warning. + + Args: + actions (Sequence[Component[C] | CBlock]): Actions to generate completions for. + ctx (Context): The current generation context. + format (type[BaseModelSubclass] | None): Not supported; ignored with a warning. + model_options (dict | None): Per-call model options. + tool_calls (bool): Ignored; tool calling is not supported on this endpoint. + + Returns: + list[ModelOutputThunk]: A list of model output thunks, one per action. + """ with instrument_generate_from_raw( backend=self, num_actions=len(actions), format=format, tool_calls=tool_calls ): diff --git a/mellea/core/backend.py b/mellea/core/backend.py index 7f971823c..82f9fae5f 100644 --- a/mellea/core/backend.py +++ b/mellea/core/backend.py @@ -160,12 +160,22 @@ async def generate_from_raw( format: A response format to used for structured outputs / constrained decoding. Note: some backends do not support this parameter. They will log warnings and continue to generate. model_options: Any model options to upsert into the defaults for this call. tool_calls: Always set to false unless supported by backend. + + Returns: + list[ModelOutputThunk]: A list of output thunks, one per action, in the same order as ``actions``. """ async def do_generate_walk( self, action: CBlock | Component | ModelOutputThunk ) -> None: - """Does the generation walk.""" + """Awaits all uncomputed ``ModelOutputThunk`` leaves reachable from ``action``. + + Traverses the component tree rooted at ``action`` via ``generate_walk``, collects + any uncomputed ``ModelOutputThunk`` nodes, and concurrently awaits them all. + + Args: + action (CBlock | Component | ModelOutputThunk): The root node to traverse. + """ _to_compute = list(generate_walk(action)) coroutines = [x.avalue() for x in _to_compute] # The following log message might get noisy. Feel free to remove if so. @@ -178,7 +188,14 @@ async def do_generate_walk( async def do_generate_walks( self, actions: list[CBlock | Component | ModelOutputThunk] ) -> None: - """Does the generation walk.""" + """Awaits all uncomputed ``ModelOutputThunk`` leaves reachable from each action in ``actions``. + + Traverses the component tree of every action in the list via ``generate_walk``, collects + all uncomputed ``ModelOutputThunk`` nodes across all actions, and concurrently awaits them. + + Args: + actions (list[CBlock | Component | ModelOutputThunk]): The list of root nodes to traverse. + """ _to_compute = [] for action in actions: _to_compute.extend(list(generate_walk(action))) @@ -200,6 +217,10 @@ def generate_walk(c: CBlock | Component | ModelOutputThunk) -> list[ModelOutputT Returns: A flat list of uncomputed ``ModelOutputThunk`` instances in the order they need to be resolved (depth-first over ``Component.parts()``). + + Raises: + ValueError: If any element encountered during traversal is not a ``CBlock``, + ``Component``, or ``ModelOutputThunk``. """ match c: case ModelOutputThunk() if not c.is_computed(): diff --git a/mellea/core/base.py b/mellea/core/base.py index 706bca72a..340014913 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -32,7 +32,18 @@ class CBlock: - """A `CBlock` is a block of content that can serve as input to or output from an LLM.""" + """A `CBlock` is a block of content that can serve as input to or output from an LLM. + + Args: + value (str | None): The underlying string content of the block. + meta (dict[str, Any] | None): Optional metadata about this block (e.g., the inference engine's + completion object). Defaults to an empty dict. + cache (bool): If ``True``, the inference engine may store the KV cache for this block. Experimental. + + Attributes: + cache (bool): Whether the inference engine may store the KV cache for this block. + value (str | None): The underlying string content of the block. + """ def __init__( self, @@ -44,9 +55,9 @@ def __init__( """Initializes the CBlock with a string and some metadata. Args: - value: the underlying value stored in this CBlock - meta: Any meta-information about this CBlock (e.g., the inference engine's Completion object). - cache: If set to `True` then this CBlock's KV cache might be stored by the inference engine. Experimental. + value (str | None): the underlying value stored in this CBlock + meta (dict[str, Any] | None): Any meta-information about this CBlock (e.g., the inference engine's Completion object). + cache (bool): If set to `True` then this CBlock's KV cache might be stored by the inference engine. Experimental. """ if value is not None and not isinstance(value, str): raise TypeError("value to a Cblock should always be a string or None") @@ -76,10 +87,26 @@ def __repr__(self) -> str: class ImageBlock(CBlock): - """A `ImageBlock` represents an image (as base64 PNG).""" + """A `ImageBlock` represents an image (as base64 PNG). + + Args: + value (str): A valid base64-encoded PNG string (with or without a data URI prefix). + meta (dict[str, Any] | None): Optional metadata to associate with this image block. + + Attributes: + value (str): The base64-encoded PNG string representing the image. + """ def __init__(self, value: str, meta: dict[str, Any] | None = None): - """Initializes the ImageBlock with a base64 PNG string representation and some metadata.""" + """Initializes the ImageBlock with a base64 PNG string representation and some metadata. + + Args: + value (str): A valid base64-encoded PNG string (with or without a data URI prefix). + meta (dict[str, Any] | None): Optional metadata to associate with this image block. + + Raises: + AssertionError: If ``value`` is not a valid base64-encoded PNG string. + """ assert self.is_valid_base64_png(value), ( "Invalid base64 string representation of image." ) @@ -87,7 +114,17 @@ def __init__(self, value: str, meta: dict[str, Any] | None = None): @staticmethod def is_valid_base64_png(s: str) -> bool: - """Checks if a string is a valid base64 string [AIA PAI Nc Hin R v1.0].""" + """Checks whether a string is a valid base64-encoded PNG image. + + Strips any data URI prefix before decoding. Adds padding characters if + necessary to make the base64 string a valid length. + + Args: + s (str): The string to validate, optionally prefixed with a data URI header. + + Returns: + bool: ``True`` if the string decodes to a PNG image, ``False`` otherwise. + """ try: # Check if the string has a data URI prefix and remove it. if "data:" in s and "base64," in s: @@ -116,7 +153,14 @@ def is_valid_base64_png(s: str) -> bool: @staticmethod def pil_to_base64(image: PILImage.Image) -> str: - """Converts a PIL image to a base64 string representation.""" + """Converts a PIL image to a base64-encoded PNG string. + + Args: + image (PILImage.Image): The PIL image to encode. + + Returns: + str: A base64-encoded string of the image serialised as PNG. + """ img_io = BytesIO() image.save(img_io, "PNG") return base64.b64encode(img_io.getvalue()).decode("utf-8") @@ -125,7 +169,18 @@ def pil_to_base64(image: PILImage.Image) -> str: def from_pil_image( cls, image: PILImage.Image, meta: dict[str, Any] | None = None ) -> ImageBlock: - """Converts a PIL image to a base64 string representation.""" + """Creates an ``ImageBlock`` from a PIL image object. + + Converts the image to a base64-encoded PNG string and wraps it in a new + ``ImageBlock`` instance. + + Args: + image (PILImage.Image): The PIL image to encode. + meta (dict[str, Any] | None): Optional metadata to associate with the block. + + Returns: + ImageBlock: A new ``ImageBlock`` containing the base64-encoded PNG. + """ image_base64 = cls.pil_to_base64(image) return cls(image_base64, meta) @@ -150,20 +205,43 @@ class Component(Protocol, Generic[S]): """A `Component` is a composite data structure that is intended to be represented to an LLM.""" def parts(self) -> list[Component | CBlock]: - """The set of all the constituent parts of the `Component`.""" + """Returns the set of all constituent sub-components and content blocks of this ``Component``. + + Returns: + list[Component | CBlock]: A list of child ``Component`` or ``CBlock`` objects that make + up this component. The list may be empty for leaf components. + + Raises: + NotImplementedError: If the concrete subclass has not overridden this method. + """ raise NotImplementedError("parts isn't implemented by default") def format_for_llm(self) -> TemplateRepresentation | str: - """Formats the `Component` into a `TemplateRepresentation` or string. + """Formats the ``Component`` into a ``TemplateRepresentation`` or plain string for LLM consumption. + + Returns: + TemplateRepresentation | str: A structured ``TemplateRepresentation`` (for components + with tools, fields, or templates) or a plain string for simple components. - Returns: a `TemplateRepresentation` or string + Raises: + NotImplementedError: If the concrete subclass has not overridden this method. """ raise NotImplementedError("format_for_llm isn't implemented by default") def parse(self, computed: ModelOutputThunk) -> S: - """Parse the expected type from a given `ModelOutputThunk`. + """Parses the expected type ``S`` from a given ``ModelOutputThunk``. + + Delegates to the component's underlying ``_parse`` method and wraps any + exception in a ``ComponentParseError`` for uniform error handling. + + Args: + computed (ModelOutputThunk): The model output thunk whose value should be parsed. + + Returns: + S: The parsed result produced by ``_parse``, typed according to the component's type parameter. - Calls the Component's underlying `._parse` function. + Raises: + ComponentParseError: If the underlying ``_parse`` call raises any exception. """ try: return self._parse(computed) @@ -176,7 +254,13 @@ def _parse(self, computed: ModelOutputThunk) -> S: class GenerateType(enum.Enum): - """Used to track what functions can be used to extract a value from a ModelOutputThunk.""" + """Used to track what functions can be used to extract a value from a ModelOutputThunk. + + Attributes: + NONE (None): No generation function has been set; the thunk is either already computed or uninitialized. + ASYNC (int): The generation function is async-compatible; ``avalue``/``astream`` may be used. + SYNC (int): The generation function is synchronous only; async extraction methods are unavailable. + """ NONE = None ASYNC = 1 @@ -184,7 +268,19 @@ class GenerateType(enum.Enum): class ModelOutputThunk(CBlock, Generic[S]): - """A `ModelOutputThunk` is a special type of `CBlock` that we know came from a model's output. It is possible to instantiate one without the output being computed yet.""" + """A `ModelOutputThunk` is a special type of `CBlock` that we know came from a model's output. It is possible to instantiate one without the output being computed yet. + + Args: + value (str | None): The raw model output string, or ``None`` if not yet computed. + meta (dict[str, Any] | None): Optional metadata from the inference engine (e.g., completion object). + parsed_repr (S | None): An already-parsed representation to attach; set when re-wrapping existing output. + tool_calls (dict[str, ModelToolCall] | None): Tool calls returned by the model alongside the text output. + + Attributes: + parsed_repr (S | None): The parsed representation of the output, non-``None`` once computed. + tool_calls (dict[str, ModelToolCall] | None): Tool calls requested by the model, if any. + value (str | None): The raw string output from the model, or ``None`` if not yet computed. + """ def __init__( self, @@ -193,7 +289,14 @@ def __init__( parsed_repr: S | None = None, tool_calls: dict[str, ModelToolCall] | None = None, ): - """Initializes as a cblock, optionally also with a parsed representation from an output formatter.""" + """Initializes as a CBlock, optionally also with a parsed representation from an output formatter. + + Args: + value (str | None): The raw model output string, or ``None`` if not yet computed. + meta (dict[str, Any] | None): Optional metadata from the inference engine (e.g., completion object). + parsed_repr (S | None): An already-parsed representation to attach; set when re-wrapping existing output. + tool_calls (dict[str, ModelToolCall] | None): Tool calls returned by the model alongside the text output. + """ super().__init__(value, meta) self.parsed_repr: S | None = parsed_repr @@ -261,7 +364,13 @@ def value(self, v: str) -> None: self._underlying_value = v async def avalue(self) -> str: - """Returns the value of the ModelOutputThunk. Can be used for both async streaming and async non-streaming. + """Returns the fully resolved value of the ModelOutputThunk, awaiting generation if necessary. + + Can be used for both async streaming and async non-streaming backends. If the + thunk is already computed the value is returned immediately. + + Returns: + str: The complete text output from the model. Raises: Exception: Propagates any errors from the underlying inference engine api request. @@ -286,12 +395,15 @@ async def avalue(self) -> str: async def astream(self) -> str: """Returns the ModelOutputThunk's partial value including the next chunk(s). Can be used for both async streaming and async non-streaming. - Returns the value of the ModelOutputThunk if streaming is done. + Returns the complete value of the ModelOutputThunk if streaming is done. **Note**: Be careful with calling this function. Only call it from one location at a time. This means you shouldn't pass a ModelOutputThunk to multiple coroutines/tasks and call astream from those coroutines/tasks simultaneously. We have considered solutions to this but are waiting until we see this error happen in a real use case. + Returns: + str: The accumulated output text up to and including the newly received chunk(s). + Raises: Exception: Propagates any errors from the underlying inference engine api request. RuntimeError: If called when the ModelOutputThunk's generate function is not async compatible. @@ -476,7 +588,20 @@ def __deepcopy__(self, memo: dict) -> ModelOutputThunk: @dataclass class ContextTurn: - """A turn of model input and model output.""" + """A turn of model input and model output. + + Args: + model_input (CBlock | Component | None): The input component or content block for this turn, + or ``None`` for an output-only partial turn. + output (ModelOutputThunk | None): The model's output thunk for this turn, + or ``None`` for an input-only partial turn. + + Attributes: + model_input (CBlock | Component | None): The input component or content block for this turn, + or ``None`` when the turn represents an output-only partial turn. + output (ModelOutputThunk | None): The model's output thunk for this turn, + or ``None`` when the turn represents an input-only partial turn. + """ model_input: CBlock | Component | None output: ModelOutputThunk | None @@ -489,6 +614,14 @@ class Context(abc.ABC): """A `Context` is used to track the state of a `MelleaSession`. A context is immutable. Every alteration leads to a new context. + + Attributes: + is_root_node (bool): ``True`` when this context is the root (empty) node of the linked list. + previous_node (Context | None): The context node from which this one was created, + or ``None`` for the root node. + node_data (Component | CBlock | None): The data associated with this context node, + or ``None`` for the root node. + is_chat_context (bool): Whether this context operates in chat (multi-turn) mode. """ _previous: Context | None @@ -508,7 +641,15 @@ def __init__(self) -> None: def from_previous( cls: type[ContextT], previous: Context, data: Component | CBlock ) -> ContextT: - """Constructs a new context from an existing context.""" + """Constructs a new context node linked to an existing context node. + + Args: + previous (Context): The existing context to extend. + data (Component | CBlock): The component or content block to associate with the new node. + + Returns: + ContextT: A new context instance whose ``previous_node`` is ``previous``. + """ assert isinstance(previous, Context), ( "Cannot create a new context from a non-Context object." ) @@ -523,7 +664,11 @@ def from_previous( @classmethod def reset_to_new(cls: type[ContextT]) -> ContextT: - """Returns an empty context for convenience.""" + """Returns a new empty (root) context. + + Returns: + ContextT: A freshly initialised root context with no data or history. + """ return cls() # Internal functions below this line. @@ -557,9 +702,16 @@ def is_chat_context(self) -> bool: # User functions below this line. def as_list(self, last_n_components: int | None = None) -> list[Component | CBlock]: - """Returns a list of the last n components in the context sorted from FIRST TO LAST. + """Returns a list of context components sorted from earliest (first) to most recent (last). If `last_n_components` is `None`, then all components are returned. + + Args: + last_n_components (int | None): Maximum number of most-recent components to include. + Pass ``None`` to return the full history. + + Returns: + list[Component | CBlock]: Components in chronological order (oldest first). """ context_list: list[Component | CBlock] = [] current_context: Context = self @@ -585,14 +737,29 @@ def as_list(self, last_n_components: int | None = None) -> list[Component | CBlo return context_list def actions_for_available_tools(self) -> list[Component | CBlock] | None: - """Provides a list of actions to extract tools from for use with during generation, or None if that's not possible. + """Provides a list of actions to extract tools from for use during generation. + + Returns ``None`` if it is not possible to construct such a list. Can be used to make + the available tools differ from the tools of all the actions in the context. Can be + overridden by subclasses. - Can be used to make the available tools differ from the tools of all the actions in the context. Can be overwritten by subclasses. + Returns: + list[Component | CBlock] | None: The list of actions whose tools should be made + available during generation, or ``None`` if unavailable. """ return self.view_for_generation() def last_output(self, check_last_n_components: int = 3) -> ModelOutputThunk | None: - """The last output thunk of the context.""" + """Returns the most recent ``ModelOutputThunk`` found within the last N context components. + + Args: + check_last_n_components (int): Number of most-recent components to search through. + Defaults to 3. + + Returns: + ModelOutputThunk | None: The most recent output thunk, or ``None`` if none is found + within the searched components. + """ for c in self.as_list(last_n_components=check_last_n_components)[::-1]: if isinstance(c, ModelOutputThunk): return c @@ -623,25 +790,54 @@ def last_turn(self) -> ContextTurn | None: @abc.abstractmethod def add(self, c: Component | CBlock) -> Context: - """Returns a new context obtained by adding `c` to this context.""" + """Returns a new context obtained by appending ``c`` to this context. + + Args: + c (Component | CBlock): The component or content block to add to the context. + + Returns: + Context: A new context node with ``c`` as its data and this context as its previous node. + """ # something along ....from_previous(self, c) ... @abc.abstractmethod def view_for_generation(self) -> list[Component | CBlock] | None: - """Provides a linear list of context components to use for generation, or None if that is not possible to construct.""" + """Provides a linear list of context components to use for generation. + + Returns ``None`` if it is not possible to construct such a list (e.g., the context + is in an inconsistent state). Concrete subclasses define the ordering and filtering logic. + + Returns: + list[Component | CBlock] | None: An ordered list of components suitable for passing + to a backend, or ``None`` if generation is not currently possible. + """ ... class AbstractMelleaTool(abc.ABC): - """Abstract base class for Mellea Tool.""" + """Abstract base class for Mellea Tool. + + Attributes: + name (str): The unique name used to identify the tool in JSON descriptions and tool-call dispatch. + as_json_tool (dict[str, Any]): A JSON-serialisable description of the tool, compatible with + the function-calling schemas expected by supported inference backends. + """ name: str """Name of the tool.""" @abc.abstractmethod def run(self, *args: Any, **kwargs: Any) -> Any: - """Runs the tool on the given arguments.""" + """Executes the tool with the provided arguments and returns the result. + + Args: + *args: Positional arguments forwarded to the tool implementation. + **kwargs: Keyword arguments forwarded to the tool implementation. + + Returns: + Any: The result produced by the tool; the concrete type depends on the implementation. + """ @property @abc.abstractmethod @@ -651,7 +847,28 @@ def as_json_tool(self) -> dict[str, Any]: @dataclass class TemplateRepresentation: - """Representing a component as a set of important attributes that can be consumed by the formatter.""" + """Representing a component as a set of important attributes that can be consumed by the formatter. + + Args: + obj (Any): The original component object being represented. + args (dict): Named arguments extracted from the component for template substitution. + tools (dict[str, AbstractMelleaTool] | None): Tools available for this representation, + keyed by the tool's function name. Defaults to ``None``. + fields (list[Any] | None): An optional ordered list of field values for positional templates. + template (str | None): An optional Jinja2 template string to use when rendering. + template_order (list[str] | None): An optional ordering hint for template sections/keys. + images (list[ImageBlock] | None): Optional list of image blocks associated with this representation. + + Attributes: + obj (Any): The original component object being represented. + args (dict): Named arguments extracted from the component for template substitution. + tools (dict[str, AbstractMelleaTool] | None): Tools available for this representation, + keyed by the tool's function name. ``None`` if the component has no tools. + fields (list[Any] | None): An optional ordered list of field values for positional templates. + template (str | None): An optional Jinja2 (or similar) template string to use when rendering. + template_order (list[str] | None): An optional ordering hint for template sections/keys. + images (list[ImageBlock] | None): Optional list of image blocks associated with this representation. + """ obj: Any args: dict[ @@ -669,10 +886,32 @@ class TemplateRepresentation: @dataclass class GenerateLog: - """A dataclass for capturing log entries. + """A dataclass for capturing log entries for a single generation call. GenerateLog provides a structured way to include various details in log entries, making it useful for maintaining detailed records of events or operations where context and additional data are significant. + + Args: + date (datetime.datetime | None): Timestamp when the generation was logged. + prompt (str | list[dict] | None): The prompt string or chat-message list sent to the model. + backend (str | None): Identifier of the inference backend used for this generation. + model_options (dict[str, Any] | None): Model configuration options applied to this call. + model_output (Any | None): The raw output returned by the backend API. + action (Component | CBlock | None): The component or block that triggered the generation. + result (ModelOutputThunk | None): The ``ModelOutputThunk`` produced by this generation call. + is_final_result (bool | None): Whether this log entry corresponds to the definitive final result. + extra (dict[str, Any] | None): Arbitrary extra metadata to attach to the log entry. + + Attributes: + date (datetime.datetime | None): Timestamp when the generation was logged. + prompt (str | list[dict] | None): The prompt string or chat-message list sent to the model. + backend (str | None): Identifier of the inference backend used for this generation. + model_options (dict[str, Any] | None): Model configuration options applied to this call. + model_output (Any | None): The raw output returned by the backend API. + action (Component | CBlock | None): The component or block that triggered the generation. + result (ModelOutputThunk | None): The ``ModelOutputThunk`` produced by this generation call. + is_final_result (bool | None): Whether this log entry corresponds to the definitive final result. + extra (dict[str, Any] | None): Arbitrary extra metadata to attach to the log entry. """ date: datetime.datetime | None = None @@ -691,6 +930,16 @@ class ModelToolCall: """A dataclass for capturing the tool calls a model wants to make. Provides a unified way to call tools post generation. + + Args: + name (str): The name of the tool the model requested to call. + func (AbstractMelleaTool): The ``AbstractMelleaTool`` instance that will be invoked. + args (Mapping[str, Any]): The keyword arguments the model supplied for the tool call. + + Attributes: + name (str): The name of the tool the model requested to call. + func (AbstractMelleaTool): The ``AbstractMelleaTool`` instance that will be invoked. + args (Mapping[str, Any]): The keyword arguments the model supplied for the tool call. """ name: str @@ -698,7 +947,11 @@ class ModelToolCall: args: Mapping[str, Any] def call_func(self) -> Any: - """A helper function for calling the function/tool represented by this object.""" + """Invokes the tool represented by this object and returns the result. + + Returns: + Any: The value returned by ``func.run(**args)``; the concrete type depends on the tool. + """ return self.func.run(**self.args) @@ -710,6 +963,9 @@ def blockify(s: str | CBlock | Component) -> CBlock | Component: Returns: A ``CBlock`` wrapping ``s`` if it was a string; otherwise ``s`` unchanged. + + Raises: + Exception: If ``s`` is not a ``str``, ``CBlock``, or ``Component``. """ # noinspection PyUnreachableCode match s: diff --git a/mellea/core/formatter.py b/mellea/core/formatter.py index 40bba08e2..faf53d5fa 100644 --- a/mellea/core/formatter.py +++ b/mellea/core/formatter.py @@ -17,5 +17,12 @@ class Formatter(abc.ABC): @abc.abstractmethod def print(self, c: Component | CBlock) -> str: - """Renders a component for input to a model.""" + """Renders a ``Component`` or ``CBlock`` into a string suitable for use as model input. + + Args: + c (Component | CBlock): The component or content block to render. + + Returns: + str: The rendered string representation of ``c``. + """ ... diff --git a/mellea/core/requirement.py b/mellea/core/requirement.py index 34f79d698..71bd276cf 100644 --- a/mellea/core/requirement.py +++ b/mellea/core/requirement.py @@ -17,7 +17,21 @@ class ValidationResult: - """ValidationResults store the output of a Requirement's validation. They can be used to return additional info from validation functions, which is useful for sampling/repairing.""" + """ValidationResults store the output of a Requirement's validation. They can be used to return additional info from validation functions, which is useful for sampling/repairing. + + Args: + result (bool): Boolean indicating whether the requirement passed. + reason (str | None): Optional human-readable explanation for the verdict. + score (float | None): Optional numeric score returned by the validator. + thunk (ModelOutputThunk | None): The ``ModelOutputThunk`` produced during LLM-as-a-Judge validation, if applicable. + context (Context | None): The context associated with the validation backend call, if applicable. + + Attributes: + reason (str | None): Human-readable explanation for the pass/fail verdict, if provided. + score (float | None): Optional numeric score returned by the validator. + thunk (ModelOutputThunk | None): The ``ModelOutputThunk`` produced during LLM-as-a-Judge validation, if applicable. + context (Context | None): The context associated with the validation backend call, if applicable. + """ def __init__( self, @@ -66,7 +80,11 @@ def context(self) -> Context | None: return self._context def as_bool(self) -> bool: - """Return a boolean value based on the result.""" + """Return a boolean value based on the validation result. + + Returns: + bool: ``True`` if the requirement passed, ``False`` otherwise. + """ return self._result def __bool__(self) -> bool: @@ -99,7 +117,26 @@ def default_output_to_bool(x: CBlock | str) -> bool: class Requirement(Component[str]): - """Requirements are a special type of Component used as input to the Validate step in Instruct/Validate/Repair patterns.""" + """Requirements are a special type of Component used as input to the Validate step in Instruct/Validate/Repair patterns. + + Args: + description (str | None): A natural-language description of the requirement. Sometimes included in + ``Instruction`` prompts; use ``check_only=True`` to suppress this. + validation_fn (Callable[[Context], ValidationResult] | None): If provided, this function is executed + instead of LLM-as-a-Judge. The ``bool()`` of its return value defines pass/fail. + output_to_bool (Callable[[CBlock | str], bool] | None): Translates LLM-as-a-Judge output to a boolean. + Defaults to a "yes"-detection heuristic. + check_only (bool): When ``True``, the requirement description is excluded from ``Instruction`` prompts. + + Attributes: + description (str | None): A natural-language description of the requirement. + output_to_bool (Callable[[CBlock | str], bool] | None): Function used to convert LLM-as-a-Judge + output into a boolean pass/fail result. + validation_fn (Callable[[Context], ValidationResult] | None): Optional custom validation + function that bypasses the LLM-as-a-Judge strategy entirely. + check_only (bool): When ``True``, the requirement description is excluded from ``Instruction`` + prompts to avoid influencing model output. + """ def __init__( self, @@ -135,7 +172,20 @@ async def validate( format: type[BaseModelSubclass] | None = None, model_options: dict | None = None, ) -> ValidationResult: - """Chooses the appropriate validation strategy and applies that strategy.""" + """Chooses the appropriate validation strategy and applies it to the given context. + + Uses ``validation_fn`` if one was provided, otherwise falls back to LLM-as-a-Judge + by generating a judgement response with the backend. + + Args: + backend (Backend): The inference backend used when the LLM-as-a-Judge strategy is selected. + ctx (Context): The context to validate, which must contain a ``ModelOutputThunk`` as its last output. + format (type[BaseModelSubclass] | None): Optional structured output format for the judgement call. + model_options (dict | None): Optional model options to pass to the backend during the judgement call. + + Returns: + ValidationResult: The result of the validation, including a boolean pass/fail and optional metadata. + """ if self.validation_fn is not None: # Python validation strategy return self.validation_fn(ctx) @@ -168,7 +218,16 @@ def parts(self) -> list[Component | CBlock]: return [] def format_for_llm(self) -> TemplateRepresentation | str: - """Some object protocol magic happens here with management of the output.""" + """Returns a ``TemplateRepresentation`` for LLM-as-a-Judge evaluation of this requirement. + + Populates the template with the requirement's ``description`` and the stored model + ``_output``. Must only be called from within a ``validate`` call for this same requirement, + after ``_output`` has been set. + + Returns: + TemplateRepresentation | str: A ``TemplateRepresentation`` containing the description + and the model output to be judged. + """ assert self._output is not None, ( "Object protocol error: should never try to templatize a Requirement except inside of a validate call for that same requirement." ) diff --git a/mellea/core/sampling.py b/mellea/core/sampling.py index 05063fdfa..6f3487c1c 100644 --- a/mellea/core/sampling.py +++ b/mellea/core/sampling.py @@ -17,7 +17,26 @@ class SamplingResult(CBlock, Generic[S]): - """Stores the results from a sampling operation. This includes successful and failed samplings.""" + """Stores the results from a sampling operation. This includes successful and failed samplings. + + Args: + result_index (int): Index into ``sample_generations`` identifying the chosen final output. + success (bool): Whether the sampling operation produced a passing result. + sample_generations (list[ModelOutputThunk[S]] | None): All output thunks generated during sampling. + sample_validations (list[list[tuple[Requirement, ValidationResult]]] | None): Per-generation validation + results; each inner list contains one tuple per requirement evaluated. + sample_actions (list[Component] | None): The actions used to produce each generation. + sample_contexts (list[Context] | None): The contexts associated with each generation. + + Attributes: + result_index (int): Index into ``sample_generations`` identifying the chosen final output. + success (bool): Whether the sampling operation produced a passing result. + sample_generations (list[ModelOutputThunk[S]]): All output thunks generated during sampling. + sample_validations (list[list[tuple[Requirement, ValidationResult]]]): Per-generation validation + results; each inner list contains one tuple per requirement evaluated. + sample_actions (list[Component]): The actions used to produce each generation. + sample_contexts (list[Context]): The contexts associated with each generation. + """ def __init__( self, diff --git a/mellea/core/utils.py b/mellea/core/utils.py index aa4c5d813..66ac33e8a 100644 --- a/mellea/core/utils.py +++ b/mellea/core/utils.py @@ -20,6 +20,11 @@ class RESTHandler(logging.Handler): Sends log records as JSON to ``/api/receive`` when the ``FLOG`` environment variable is set. Failures are silently suppressed to avoid disrupting the application. + + Attributes: + api_url (str): The URL of the REST endpoint that receives log records. + method (str): The HTTP method used when sending records (default ``"POST"``). + headers (dict): HTTP headers sent with each request; defaults to ``{"Content-Type": "application/json"}``. """ def __init__( @@ -32,7 +37,13 @@ def __init__( self.headers = headers or {"Content-Type": "application/json"} def emit(self, record: logging.LogRecord) -> None: - """Attempts to emit a record to FLOG, or silently fails.""" + """Forwards a log record to the REST endpoint when the ``FLOG`` environment variable is set. + + Silently suppresses any network or HTTP errors to avoid disrupting the application. + + Args: + record (logging.LogRecord): The log record to forward. + """ if os.environ.get("FLOG"): log_data = self.format(record) try: @@ -56,7 +67,14 @@ class JsonFormatter(logging.Formatter): """ def format(self, record): # type: ignore - """Formats record as a JSON serializable object.""" + """Formats a log record as a JSON-serialisable dictionary. + + Includes timestamp, level, message, module, function name, line number, + process ID, thread ID, and exception info if present. + + Args: + record (logging.LogRecord): The log record to format. + """ log_record = { "timestamp": self.formatTime(record, self.datefmt), "level": record.levelname, @@ -73,7 +91,17 @@ def format(self, record): # type: ignore class CustomFormatter(logging.Formatter): - """A nice custom formatter copied from [Sergey Pleshakov's post on StackOverflow](https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output).""" + """A nice custom formatter copied from [Sergey Pleshakov's post on StackOverflow](https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output). + + Attributes: + cyan (str): ANSI escape code for cyan text, used for DEBUG messages. + grey (str): ANSI escape code for grey text, used for INFO messages. + yellow (str): ANSI escape code for yellow text, used for WARNING messages. + red (str): ANSI escape code for red text, used for ERROR messages. + bold_red (str): ANSI escape code for bold red text, used for CRITICAL messages. + reset (str): ANSI escape code to reset text colour. + FORMATS (dict): Mapping from logging level integer to the colour-formatted format string. + """ cyan = "\033[96m" # Cyan grey = "\x1b[38;20m" @@ -92,7 +120,14 @@ class CustomFormatter(logging.Formatter): } def format(self, record: logging.LogRecord) -> str: - """The format fn.""" + """Formats a log record using a colour-coded ANSI format string based on the record's log level. + + Args: + record (logging.LogRecord): The log record to format. + + Returns: + str: The formatted log record string with ANSI colour codes applied. + """ log_fmt = self.FORMATS.get(record.levelno) formatter = logging.Formatter(log_fmt, datefmt="%H:%M:%S") return formatter.format(record) @@ -105,6 +140,17 @@ class FancyLogger: defaults to ``INFO`` but can be raised to ``DEBUG`` by setting the ``DEBUG`` environment variable. When the ``FLOG`` environment variable is set, records are also forwarded to a local ``/api/receive`` REST endpoint via ``RESTHandler``. + + Attributes: + logger (logging.Logger | None): The shared ``logging.Logger`` instance; ``None`` until first call to ``get_logger()``. + CRITICAL (int): Numeric level for critical log messages (50). + FATAL (int): Alias for ``CRITICAL`` (50). + ERROR (int): Numeric level for error log messages (40). + WARNING (int): Numeric level for warning log messages (30). + WARN (int): Alias for ``WARNING`` (30). + INFO (int): Numeric level for informational log messages (20). + DEBUG (int): Numeric level for debug log messages (10). + NOTSET (int): Numeric level meaning no level is set (0). """ logger = None diff --git a/mellea/formatters/chat_formatter.py b/mellea/formatters/chat_formatter.py index d930fde71..6ccfced05 100644 --- a/mellea/formatters/chat_formatter.py +++ b/mellea/formatters/chat_formatter.py @@ -22,7 +22,22 @@ class ChatFormatter(Formatter): """Formatter used by Legacy backends to format Contexts as Messages.""" def to_chat_messages(self, cs: list[Component | CBlock]) -> list[Message]: - """Helper method that converts a linearized chat history into a list of messages. The purpose of this helper is to prepare a sequence of Messages for input to a chat endpoint.""" + """Convert a linearized chat history into a list of chat messages. + + Iterates over each element in the context history and converts it to a + ``Message`` with an appropriate role. ``ModelOutputThunk`` instances are + treated as assistant responses, while all other ``Component`` and + ``CBlock`` objects default to the ``user`` role. Image attachments and + parsed structured outputs are handled transparently. + + Args: + cs (list[Component | CBlock]): The linearized sequence of context + components and code blocks to convert. + + Returns: + list[Message]: A list of ``Message`` objects ready for submission to + a chat completion endpoint. + """ def _to_msg(c: Component | CBlock) -> Message: role: Message.Role = "user" # default to `user`; see ModelOutputThunk below for when the role changes. diff --git a/mellea/formatters/granite/base/io.py b/mellea/formatters/granite/base/io.py index 1e7bc6552..ece5ecce0 100644 --- a/mellea/formatters/granite/base/io.py +++ b/mellea/formatters/granite/base/io.py @@ -32,14 +32,17 @@ def transform( the string representation of the tokens that should be sent to the model to implement said request. - :param chat_completion: Structured representation of the inputs - :param add_generation_prompt: If ``True``, the returned prompt string will - contain a prefix of the next assistant response for use as a prompt to a - generation request. Otherwise, the prompt will only contain the messages and - documents in ``input``. - - :returns: String that can be passed to the model's tokenizer to create a prompt - for generation. + Args: + chat_completion (ChatCompletion): Structured representation of the inputs + to the chat completion request. + add_generation_prompt (bool): If ``True``, the returned prompt string will + contain a prefix of the next assistant response for use as a prompt to a + generation request. Otherwise, the prompt will only contain the messages + and documents in ``chat_completion``. Defaults to ``True``. + + Returns: + str: String that can be passed to the model's tokenizer to create a prompt + for generation. """ @@ -61,14 +64,16 @@ def transform( Convert the model output generated into a structured representation of the information. - :param model_output: String output of the a generation request, potentially - incomplete if it was a streaming request - :param chat_completion: The chat completion request that produced - ``model_output``. Parameters of the request can determine how the output - should be decoded. + Args: + model_output (str): String output of the generation request, potentially + incomplete if it was a streaming request. + chat_completion (ChatCompletion | None): The chat completion request that + produced ``model_output``. Parameters of the request can determine how + the output should be decoded. Defaults to ``None``. - :returns: The parsed output so far, as an instance of :class:`AssistantMessage` - possibly with model-specific extension fields. + Returns: + AssistantMessage: The parsed output so far, as an instance of + :class:`AssistantMessage` possibly with model-specific extension fields. """ @@ -87,10 +92,19 @@ def transform( Rewrite a chat completion request into another chat completion request. Does not modify the original :class:`ChatCompletion` object. - :param chat_completion: Original chat completion request, either as a dataclass - or as the JSON representation of one. + Args: + chat_completion (ChatCompletion | str | dict): Original chat completion + request, either as a :class:`ChatCompletion` dataclass, the JSON string + representation, or a plain dictionary. + **kwargs: Additional keyword arguments forwarded to the underlying + :meth:`_transform` implementation. - :returns: Rewritten copy of ``chat_completion``. + Returns: + ChatCompletion: Rewritten copy of the original chat completion request. + + Raises: + TypeError: If ``chat_completion`` is not a :class:`ChatCompletion` object, + a JSON string, or a dictionary. """ if isinstance(chat_completion, str): chat_completion = ChatCompletion.model_validate_json(chat_completion) @@ -131,15 +145,24 @@ def transform( chat_completion_response: ChatCompletionResponse | dict | pydantic.BaseModel, chat_completion: ChatCompletion | None = None, ) -> ChatCompletionResponse: - """Parse the result of a chat completion. - - :param chat_completion_response: Response to a chat completion request as - a parsed dataclass, parsed JSON value, or OpenAI dataclass - :param chat_completion: The chat completion request that produced - ``chat_completion_response``. Required by some implementations in order - to decode references to part of the original request. - - :returns: Rewritten copy of ``chat_completion``. + """Parse and post-process the result of a chat completion request. + + Args: + chat_completion_response (ChatCompletionResponse | dict | pydantic.BaseModel): + Response to a chat completion request, provided as a parsed + :class:`ChatCompletionResponse` dataclass, a raw dictionary, or + another Pydantic model. + chat_completion (ChatCompletion | None): The original chat completion + request that produced ``chat_completion_response``. Required by + some implementations to decode references back to the original + request. Defaults to ``None``. + + Returns: + ChatCompletionResponse: Post-processed copy of the chat completion + response with model-specific transformations applied. + + Raises: + TypeError: If ``chat_completion_response`` is not a supported type. """ # Convert from over-the-wire format if necessary if isinstance(chat_completion_response, dict): @@ -179,11 +202,13 @@ class Retriever(abc.ABC): @abc.abstractmethod def retrieve(self, query: str, top_k: int = 10) -> list[Document]: - """Retrieve the top k matches of a query from the corpus. + """Retrieve the top-k matching documents for a query from the corpus. - :param query: Query string to use for lookup - :param top_k: Number of top-k results to return + Args: + query (str): Query string to use for lookup. + top_k (int): Maximum number of results to return. Defaults to ``10``. - :returns: Pyarrow table with fields: "id", "title", "url", "begin", "end", - "text", "score" + Returns: + list[Document]: List of the top-k matching :class:`Document` objects, + each with fields such as ``text``, ``title``, and ``doc_id``. """ diff --git a/mellea/formatters/granite/base/optional.py b/mellea/formatters/granite/base/optional.py index 9bd6deaf2..705f1bd2f 100644 --- a/mellea/formatters/granite/base/optional.py +++ b/mellea/formatters/granite/base/optional.py @@ -47,6 +47,10 @@ def nltk_check(feature_name: str): Args: feature_name: Name of the feature that requires NLTK, used in the error message. + + Raises: + ImportError: If the ``nltk`` package is not installed, re-raised with + a descriptive message and installation instructions. """ try: yield diff --git a/mellea/formatters/granite/base/types.py b/mellea/formatters/granite/base/types.py index 8446b2525..8c747c012 100644 --- a/mellea/formatters/granite/base/types.py +++ b/mellea/formatters/granite/base/types.py @@ -127,7 +127,11 @@ def _keep_these_fields(self): class UserMessage(_ChatMessageBase): - """User message for an IBM Granite model chat completion request.""" + """User message for an IBM Granite model chat completion request. + + Attributes: + role (str): Always ``"user"``, identifying the message sender. + """ role: Literal["user"] = "user" @@ -137,13 +141,29 @@ class DocumentMessage(_ChatMessageBase): Document message for an IBM Granite model (from the Ollama library) chat completion request. + + Attributes: + role (str): A string matching the pattern ``"document "``, + identifying this message as a document fragment. """ role: Annotated[str, StringConstraints(pattern=r"^document .+$")] class ToolCall(pydantic.BaseModel, NoDefaultsMixin): - """Tool call entry format.""" + """Represents a single tool-call entry produced by an assistant message. + + Captures the identifier, name, and arguments of a tool invocation returned + by the model during a chat completion response. + + Attributes: + id (str | None): An optional unique identifier for this tool call, + used to correlate calls with their results. + name (str): The name of the tool to invoke. + arguments (dict[str, Any] | None): A mapping of argument names to + values, conforming to the parameter schema of the associated tool + definition. + """ id: str | None = None name: str @@ -158,6 +178,13 @@ class AssistantMessage(_ChatMessageBase): Lowest-common-denominator assistant message for an IBM Granite model chat completion request. + + Attributes: + role (str): Always ``"assistant"``, identifying the message sender. + tool_calls (list[ToolCall] | None): Optional list of tool calls + requested by the assistant during this turn. + reasoning_content (str | None): Optional chain-of-thought or reasoning + text produced by the model before the final response. """ role: Literal["assistant"] = "assistant" @@ -170,6 +197,10 @@ class ToolResultMessage(_ChatMessageBase): Message containing the result of a tool call in an IBM Granite model chat completion request. + + Attributes: + role (str): Always ``"tool"``, identifying this as a tool-result message. + tool_call_id (str): The identifier of the tool call this message responds to. """ role: Literal["tool"] = "tool" @@ -177,13 +208,21 @@ class ToolResultMessage(_ChatMessageBase): class SystemMessage(_ChatMessageBase): - """System message for an IBM Granite model chat completion request.""" + """System message for an IBM Granite model chat completion request. + + Attributes: + role (str): Always ``"system"``, identifying this as a system-level instruction. + """ role: Literal["system"] = "system" class DeveloperMessage(_ChatMessageBase): - """Developer system message for a chat completion request.""" + """Developer system message for a chat completion request. + + Attributes: + role (str): Always ``"developer"``, identifying this as a developer-role message. + """ role: Literal["developer"] = "developer" @@ -201,7 +240,15 @@ class DeveloperMessage(_ChatMessageBase): class ToolDefinition(pydantic.BaseModel, NoDefaultsMixin): - """An entry in the ``tools`` list in an IBM Granite model chat completion request.""" + """An entry in the ``tools`` list in an IBM Granite model chat completion request. + + Attributes: + name (str): The name used to identify and invoke the tool. + description (str | None): An optional human-readable description of + what the tool does. + parameters (dict[str, Any] | None): An optional JSON Schema object + describing the tool's input parameters. + """ name: str description: str | None = None @@ -216,6 +263,12 @@ class Document(pydantic.BaseModel, NoDefaultsMixin): RAG documents, which in practice are usually snippets drawn from larger documents. + + Attributes: + text (str): The textual content of the document snippet. + title (str | None): An optional title for the document. + doc_id (str | None): An optional string identifier for the document, + required by some backends such as vLLM. """ text: str @@ -230,6 +283,11 @@ class ChatTemplateKwargs(pydantic.BaseModel): Values that can appear in the ``chat_template_kwargs`` portion of a valid chat completion request for a Granite model. + + Attributes: + model_config (ConfigDict): Pydantic model configuration allowing + arbitrary types and extra fields to be passed through to + model-specific I/O processors. """ model_config = pydantic.ConfigDict( @@ -246,6 +304,16 @@ class VLLMExtraBody(pydantic.BaseModel, NoDefaultsMixin): Elements of `vllm.entrypoints.openai.protocol.ChatCompletionRequest` that are not part of OpenAI's protocol and need to be stuffed into the "extra_body" parameter of a chat completion request. + + Attributes: + documents (list[Document] | None): RAG documents made accessible to + the model during generation, if the template supports RAG. + add_generation_prompt (bool): When ``True``, the generation prompt is + appended to the rendered chat template. Defaults to ``True``. + chat_template_kwargs (ChatTemplateKwargs | None): Additional keyword + arguments forwarded to the chat template renderer. + structured_outputs (dict | None): Optional JSON schema that constrains + the model's output format. """ documents: list[Document] | None = Field( @@ -304,6 +372,17 @@ class ChatCompletion(pydantic.BaseModel, NoDefaultsMixin): See the class `vllm.entrypoints.openai.protocol.ChatCompletionRequest` for more information. + + Attributes: + messages (list[ChatMessage]): The ordered list of chat messages forming + the conversation history. + model (str | None): An optional model identifier specifying which model + to use for the completion. + tools (list[ToolDefinition] | None): An optional list of tool + definitions made available to the model during generation. + extra_body (VLLMExtraBody | None): Optional vLLM-specific parameters + not covered by the OpenAI protocol, such as documents and + chat-template kwargs. """ messages: list[ChatMessage] @@ -421,6 +500,13 @@ class Logprob(pydantic.BaseModel, NoDefaultsMixin): See the class `vllm.entrypoints.openai.protocol.Logprob` for more information. + + Attributes: + logprob (float): The log-probability value for this token. + rank (int | None): The rank of this token among the top candidates, + if available. + decoded_token (str | None): The decoded string representation of the + token, if available. """ logprob: float @@ -438,6 +524,13 @@ class ChatCompletionLogProb(pydantic.BaseModel, NoDefaultsMixin): See the class `vllm.entrypoints.openai.protocol.ChatCompletionLogProb` for more information. + + Attributes: + token (str): The decoded token string. + logprob (float): The log-probability of the token. Defaults to + ``-9999.0`` when not returned by the server. + bytes (list[int] | None): The UTF-8 byte values of the token, + if provided by the server. """ token: str @@ -453,6 +546,10 @@ class ChatCompletionLogProbsContent(ChatCompletionLogProb): See the class `vllm.entrypoints.openai.protocol.ChatCompletionLogProbsContent` for more information. + + Attributes: + top_logprobs (list[ChatCompletionLogProb]): The list of top-k + candidate tokens and their log-probabilities at this position. """ top_logprobs: list[ChatCompletionLogProb] = Field(default_factory=list) @@ -466,6 +563,11 @@ class ChatCompletionLogProbs(pydantic.BaseModel, NoDefaultsMixin): See the class `vllm.entrypoints.openai.protocol.ChatCompletionLogProbs` for more information. + + Attributes: + content (list[ChatCompletionLogProbsContent] | None): Per-token + log-probability entries for each generated token, or ``None`` if + logprobs were not requested. """ content: list[ChatCompletionLogProbsContent] | None = None @@ -479,6 +581,14 @@ class ChatCompletionResponseChoice(pydantic.BaseModel, NoDefaultsMixin): See the class `vllm.entrypoints.openai.protocol.ChatCompletionResponseChoice` for more information. + + Attributes: + index (int): The zero-based index of this choice in the response. + message (ChatMessage): The generated message for this choice. + logprobs (ChatCompletionLogProbs | None): Token log-probabilities for + this choice, if they were requested. + finish_reason (str | None): The reason the model stopped generating. + Defaults to ``"stop"`` per the OpenAI specification. """ index: int @@ -496,6 +606,15 @@ class ChatCompletionResponse(pydantic.BaseModel, NoDefaultsMixin): See the class `vllm.entrypoints.openai.protocol.ChatCompletionResponse` for more information. + + Attributes: + choices (list[ChatCompletionResponseChoice]): The list of generated + response choices returned by the model. + prompt_logprobs (list[dict[int, Logprob] | None] | None): Per-token + prompt log-probabilities returned by vLLM, if requested. This + field is not part of the OpenAI specification. + model_config (ConfigDict): Pydantic configuration allowing extra fields + to be passed through when transforming data. """ choices: list[ChatCompletionResponseChoice] diff --git a/mellea/formatters/granite/base/util.py b/mellea/formatters/granite/base/util.py index c47316d69..4d02ef9c6 100644 --- a/mellea/formatters/granite/base/util.py +++ b/mellea/formatters/granite/base/util.py @@ -52,6 +52,10 @@ def nltk_check(feature_name: str): Args: feature_name: Name of the feature that requires NLTK, used in the error message. + + Raises: + ImportError: If the ``nltk`` package is not installed, re-raised with + a descriptive message and installation instructions. """ try: yield @@ -109,6 +113,11 @@ def load_transformers_lora(local_or_remote_path): Returns: Tuple of ``(model, tokenizer)`` where ``model`` is the loaded LoRA model and ``tokenizer`` is the corresponding HuggingFace tokenizer. + + Raises: + ImportError: If ``peft`` or ``transformers`` packages are not installed. + NotImplementedError: If ``local_or_remote_path`` does not exist locally + (remote loading from the Hugging Face Hub is not yet implemented). """ with import_optional("peft"): # Third Party @@ -146,6 +155,14 @@ def chat_completion_request_to_transformers_inputs( Tuple of ``(generate_input, other_input)`` where ``generate_input`` contains kwargs to pass directly to ``generate()`` and ``other_input`` contains additional parameters for ``generate_with_transformers``. + + Raises: + ImportError: If ``torch``, ``transformers``, or ``xgrammar`` packages + are not installed (the latter only when constrained decoding is used). + TypeError: If ``tokenizer.apply_chat_template()`` returns an unexpected type. + ValueError: If padding or end-of-sequence token IDs cannot be determined + from the tokenizer, or if a constrained-decoding request is made + without passing a ``tokenizer`` or ``model`` argument. """ with import_optional("torch"): # Third Party diff --git a/mellea/formatters/granite/granite3/granite32/input.py b/mellea/formatters/granite/granite3/granite32/input.py index d548f297d..a5333cf9f 100644 --- a/mellea/formatters/granite/granite3/granite32/input.py +++ b/mellea/formatters/granite/granite3/granite32/input.py @@ -222,20 +222,39 @@ def _remove_special_tokens(cls, text: str) -> str: @classmethod def sanitize(cls, chat_completion, parts="all"): - """Sanitize chat completion. + """Sanitize the chat completion by removing Granite 3.2 special tokens. - Call the parent sanitize function with the specific remove special - tokens function for this Granite version. + Args: + chat_completion: The chat completion request to sanitize. + parts (str): Which parts of the chat completion to sanitize; + defaults to ``"all"``. + + Returns: + The sanitized chat completion with all Granite 3.2 special tokens + removed from the specified parts. """ return super()._sanitize(chat_completion, cls._remove_special_tokens, parts) def transform( self, chat_completion: ChatCompletion, add_generation_prompt: bool = True ) -> str: - """Transform chat completion to prompt string. - - Downcast to a Model-specific request type with possible additional fields. - This operation also performs additional validation. + """Transform the chat completion request into a Granite 3.2 prompt string. + + Args: + chat_completion (ChatCompletion): The structured chat completion request + to convert into a tokenizer-ready prompt string. + add_generation_prompt (bool): When ``True``, appends the assistant role + header to the end of the prompt to trigger generation. Defaults to + ``True``. + + Returns: + str: The prompt string formatted for the Granite 3.2 model tokenizer. + + Raises: + ValueError: If conflicting options are specified, such as enabling + ``thinking`` mode together with documents, tools, or a custom + system message; or enabling ``citations`` or ``hallucinations`` + with a custom system message. """ chat_completion = Granite32ChatCompletion.model_validate( chat_completion.model_dump() diff --git a/mellea/formatters/granite/granite3/granite32/output.py b/mellea/formatters/granite/granite3/granite32/output.py index a4e3ee6dd..e3c4b44f0 100644 --- a/mellea/formatters/granite/granite3/granite32/output.py +++ b/mellea/formatters/granite/granite3/granite32/output.py @@ -614,10 +614,23 @@ class Granite32OutputProcessor(OutputProcessor): def transform( self, model_output: str, chat_completion: ChatCompletion | None = None ) -> AssistantMessage: - """Transform model output to assistant message. - - Downcast to a Granite-specific request type with possible additional fields. - This operation also performs additional validation. + """Parse Granite 3.2 model output into a structured assistant message. + + Args: + model_output (str): Raw text output from the Granite 3.2 model. + chat_completion (ChatCompletion | None): The original chat completion + request that produced ``model_output``. Used to determine which + output features (thinking, tools, citations, hallucinations) to + parse. Defaults to ``None``. + + Returns: + AssistantMessage: A :class:`Granite3AssistantMessage` containing the + parsed response text, optional tool calls, chain-of-thought + reasoning, citations, documents, and hallucination annotations. + + Raises: + ValueError: If parsing citations, documents, or hallucinations from + the model output fails. """ if chat_completion is None: chat_completion = Granite32ChatCompletion(messages=[]) diff --git a/mellea/formatters/granite/granite3/granite33/input.py b/mellea/formatters/granite/granite3/granite33/input.py index 26f058dbe..2bb8f899d 100644 --- a/mellea/formatters/granite/granite3/granite33/input.py +++ b/mellea/formatters/granite/granite3/granite33/input.py @@ -142,7 +142,17 @@ def _remove_special_tokens(cls, text: str) -> str: @classmethod def sanitize(cls, chat_completion, parts="all"): - """Sanitize the chat completion by removing special tokens.""" + """Sanitize the chat completion by removing Granite 3.3 special tokens. + + Args: + chat_completion: The chat completion request to sanitize. + parts (str): Which parts of the chat completion to sanitize; + defaults to ``"all"``. + + Returns: + The sanitized chat completion with all Granite 3.3 special tokens + removed from the specified parts. + """ # Call the parent sanitize function with the specific remove special # tokens function for this Granite version. return super()._sanitize(chat_completion, cls._remove_special_tokens, parts) @@ -150,7 +160,24 @@ def sanitize(cls, chat_completion, parts="all"): def transform( self, chat_completion: ChatCompletion, add_generation_prompt: bool = True ) -> str: - """Transform the chat completion into a prompt string.""" + """Transform the chat completion request into a Granite 3.3 prompt string. + + Args: + chat_completion (ChatCompletion): The structured chat completion request + to convert into a tokenizer-ready prompt string. + add_generation_prompt (bool): When ``True``, appends the assistant role + header to the end of the prompt to trigger generation. Defaults to + ``True``. + + Returns: + str: The prompt string formatted for the Granite 3.3 model tokenizer. + + Raises: + ValueError: If conflicting options are specified, such as enabling + ``thinking`` mode together with documents, tools, or a custom + system message; or enabling ``citations`` or ``hallucinations`` + with a custom system message. + """ # Downcast to a Granite-specific request type with possible additional fields. # This operation also performs additional validation. chat_completion = Granite33ChatCompletion.model_validate( diff --git a/mellea/formatters/granite/granite3/granite33/output.py b/mellea/formatters/granite/granite3/granite33/output.py index aeda6175d..34893de22 100644 --- a/mellea/formatters/granite/granite3/granite33/output.py +++ b/mellea/formatters/granite/granite3/granite33/output.py @@ -523,7 +523,24 @@ class Granite33OutputProcessor(OutputProcessor): def transform( self, model_output: str, chat_completion: ChatCompletion | None = None ) -> AssistantMessage: - """Transform model output into an AssistantMessage.""" + """Parse Granite 3.3 model output into a structured assistant message. + + Args: + model_output (str): Raw text output from the Granite 3.3 model. + chat_completion (ChatCompletion | None): The original chat completion + request that produced ``model_output``. Used to determine which + output features (thinking, tools, citations, hallucinations) to + parse. Defaults to ``None``. + + Returns: + AssistantMessage: A :class:`Granite3AssistantMessage` containing the + parsed response text, optional tool calls, chain-of-thought + reasoning, citations, documents, and hallucination annotations. + + Raises: + ValueError: If parsing citations, documents, or hallucinations from + the model output fails. + """ # Downcast to a Granite-specific request type with possible additional fields. # This operation also performs additional validation. if chat_completion is None: diff --git a/mellea/formatters/granite/granite3/types.py b/mellea/formatters/granite/granite3/types.py index c692cf351..4182537c9 100644 --- a/mellea/formatters/granite/granite3/types.py +++ b/mellea/formatters/granite/granite3/types.py @@ -20,7 +20,19 @@ class Hallucination(pydantic.BaseModel): - """Hallucination data as returned by the model output parser.""" + """Hallucination data as returned by the model output parser. + + Attributes: + hallucination_id (str): Unique identifier for the hallucination entry. + risk (str): Risk level of the hallucination, e.g. ``"low"`` or ``"high"``. + reasoning (str | None): Optional model-provided reasoning for why this + sentence was flagged. + response_text (str): The portion of the response text that is flagged. + response_begin (int): Start character offset of ``response_text`` within + the full response string. + response_end (int): End character offset (exclusive) of ``response_text`` + within the full response string. + """ hallucination_id: str risk: str @@ -31,7 +43,23 @@ class Hallucination(pydantic.BaseModel): class Citation(pydantic.BaseModel): - """Citation data as returned by the model output parser.""" + """Citation data as returned by the model output parser. + + Attributes: + citation_id (str): Unique identifier assigned to this citation. + doc_id (str): Identifier of the source document being cited. + context_text (str): Verbatim text from the source document that is cited. + context_begin (int): Start character offset of ``context_text`` within + the source document. + context_end (int): End character offset (exclusive) of ``context_text`` + within the source document. + response_text (str): The portion of the response text that makes this + citation. + response_begin (int): Start character offset of ``response_text`` within + the response string. + response_end (int): End character offset (exclusive) of ``response_text`` + within the response string. + """ citation_id: str doc_id: str @@ -44,7 +72,22 @@ class Citation(pydantic.BaseModel): class Granite3Controls(pydantic.BaseModel): - """Granite 3.x controls record.""" + """Control flags for Granite 3.x model output behaviour. + + Specifies which optional output features the model should produce, such as + inline citations, hallucination risk annotations, response length constraints, + and originality style. + + Attributes: + citations (bool | None): When ``True``, instructs the model to annotate + factual claims with inline citation markers. + hallucinations (bool | None): When ``True``, instructs the model to + append a list of sentences that may be hallucinated. + length (str | None): Requested response length; must be ``"short"``, + ``"long"``, or ``None`` for no constraint. + originality (str | None): Requested response originality style; must be + ``"extractive"``, ``"abstractive"``, or ``None``. + """ citations: bool | None = None hallucinations: bool | None = None @@ -75,7 +118,18 @@ def _validate_originality(cls, value: str | None) -> str | None: class Granite3Kwargs(ChatTemplateKwargs, NoDefaultsMixin): - """Granite 3 keyword arguments.""" + """Chat template keyword arguments specific to IBM Granite 3.x models. + + Extends :class:`ChatTemplateKwargs` with Granite 3-specific options for + output control flags and chain-of-thought (thinking) mode. + + Attributes: + controls (Granite3Controls | None): Optional output control flags that + enable or configure citations, hallucination detection, response + length, and originality style. + thinking (bool): When ``True``, enables chain-of-thought reasoning mode. + Defaults to ``False``. + """ controls: Granite3Controls | None = None thinking: bool = False @@ -89,14 +143,24 @@ class Granite3ChatCompletion(GraniteChatCompletion): """ def controls(self) -> Granite3Controls: - """:returns: An appropriate Granite 3 controls record for the chat completion""" + """Return the Granite 3 controls record for this chat completion request. + + Returns: + Granite3Controls: The controls record from the chat template kwargs, + or an empty :class:`Granite3Controls` if none were specified. + """ kwargs = self.extra_body.chat_template_kwargs if self.extra_body else None if kwargs and isinstance(kwargs, Granite3Kwargs) and kwargs.controls: return kwargs.controls return Granite3Controls() def thinking(self) -> bool: - """:returns: ``True`` if thinking mode is enabled for this request""" + """Return whether chain-of-thought thinking mode is enabled. + + Returns: + bool: ``True`` if the ``thinking`` flag is set in the chat template + kwargs; ``False`` otherwise. + """ kwargs = self.extra_body.chat_template_kwargs if self.extra_body else None return bool(kwargs and isinstance(kwargs, Granite3Kwargs) and kwargs.thinking) @@ -168,7 +232,19 @@ def _validate_inputs_messages(cls, messages: list) -> list: class Granite3AssistantMessage(AssistantMessage): - """An assistant message with Granite 3 specific fields.""" + """An assistant message with Granite 3 specific fields. + + Attributes: + reasoning_content (str | None): Optional chain-of-thought reasoning text + produced before the final response. + citations (list[Citation] | None): Optional list of citations parsed from + the model output. + documents (list[Document] | None): Optional list of documents referenced + in the model output. + hallucinations (list[Hallucination] | None): Optional list of hallucination + annotations parsed from the model output. + stop_reason (str | None): Optional reason the model stopped generating. + """ reasoning_content: str | None = None citations: list[Citation] | None = None diff --git a/mellea/formatters/granite/intrinsics/input.py b/mellea/formatters/granite/intrinsics/input.py index 1f06454fa..fe4fbdaf4 100644 --- a/mellea/formatters/granite/intrinsics/input.py +++ b/mellea/formatters/granite/intrinsics/input.py @@ -94,6 +94,12 @@ def move_documents_to_message( A copy of ``chat_completion`` with any documents under ``extra_body`` moved to the first message. Returned type will be the same as the input type. May return original object if no edits are necessary. + + Raises: + TypeError: If ``chat_completion`` is not a :class:`ChatCompletion` or + ``dict``. + ValueError: If ``how`` is not one of ``"string"``, ``"json"``, or + ``"roles"``. """ if isinstance(chat_completion, ChatCompletion): should_return_dataclass = True @@ -163,6 +169,33 @@ class IntrinsicsRewriter(ChatCompletionRewriter): General-purpose chat completion rewriter for use with models that implement LLM intrinsics. Reads parameters of the model's input and output formats from a YAML configuration file and edits the input chat completion appropriately. + + Args: + config_file (str | pathlib.Path | None): Path to the YAML configuration file for the + target intrinsic. Mutually exclusive with ``config_dict``. + config_dict (dict | None): Inline configuration dictionary. Mutually exclusive with + ``config_file``. + model_name (str | None): Optional model name used to locate model-specific overrides + within the configuration. + + Attributes: + config (dict): Parsed YAML configuration file for the target intrinsic. + response_format (dict): JSON Schema of the expected response format. + parameters (dict): Additional parameters (key-value pairs) that this + rewriter adds to all chat completion requests. + extra_body_parameters (dict): Extended vLLM-specific parameters that go + under the ``extra_body`` element of each request. These are merged + with any existing ``extra_body`` content in incoming requests. + instruction (str | None): Optional instruction template. When present, + a new user message is appended with the formatted instruction. + sentence_boundaries (dict[str, str] | None): Optional sentence-boundary + marking specification, mapping location strings (``"last_message"`` + or ``"documents"``) to marker prefixes (e.g. ``"c"`` produces + ````, ````, …). + docs_as_message (str | None): Optional specification for moving + documents from ``extra_body/documents`` to a user message at the + start of the messages list. Value must be ``"string"``, ``"json"``, + or ``"roles"``. """ config: dict @@ -208,11 +241,15 @@ def __init__( """Initialize the IntrinsicsRewriter. Args: - config_file: Optional location of the YAML configuration file. - config_dict: Optional pre-parsed contents of the YAML configuration file - (result of ``yaml.safe_load()``). - model_name: Optional model name to inject into chat completion requests; - ``None`` uses the default from the configuration. + config_file (str | pathlib.Path | None): Optional location of the + YAML configuration file. Exactly one of ``config_file`` and + ``config_dict`` must be provided. + config_dict (dict | None): Optional pre-parsed contents of the YAML + configuration file (result of ``yaml.safe_load()``). Exactly one + of ``config_file`` and ``config_dict`` must be provided. + model_name (str | None): Optional model name to inject into chat + completion requests. ``None`` uses the default from the + configuration. Defaults to ``None``. """ config = make_config_dict(config_file, config_dict) if config is None: diff --git a/mellea/formatters/granite/intrinsics/json_util.py b/mellea/formatters/granite/intrinsics/json_util.py index 7e89d4e68..573802680 100644 --- a/mellea/formatters/granite/intrinsics/json_util.py +++ b/mellea/formatters/granite/intrinsics/json_util.py @@ -38,7 +38,16 @@ class JsonLiteralWithPosition(pydantic.BaseModel): - """JSON literal value with its position in the source string.""" + """JSON literal value with its position in the source string. + + Attributes: + value (str | bool | int | float): The parsed Python value of the JSON + literal (string, boolean, integer, or float). + begin (int): Start offset (inclusive) of the literal within the source + JSON string. + end (int): End offset (exclusive) of the literal within the source + JSON string. + """ value: str | bool | int | float begin: int @@ -146,6 +155,10 @@ def reparse_value(tokens, offset) -> tuple[Any, int]: Returns: Tuple of ``(parsed_value, next_offset)``. + + Raises: + ValueError: If an unexpected delimiter token or unknown token type is + encountered at the current offset. """ begin, end, value, type_ = tokens[offset] if type_ == "delim": @@ -164,7 +177,25 @@ def reparse_value(tokens, offset) -> tuple[Any, int]: def reparse_object(tokens, offset) -> tuple[dict, int]: - """Subroutine for handling object parsing inside reparse_value().""" + """Parse a JSON object from the token stream, starting after the opening ``{``. + + Subroutine called by :func:`reparse_value` when an opening curly brace is + encountered. Consumes tokens until the matching closing ``}`` is found. + + Args: + tokens: Token stream as produced by ``tokenize_json()``. + offset (int): Token offset immediately after the opening ``{`` delimiter. + + Returns: + tuple[dict, int]: A tuple of ``(parsed_dict, next_offset)`` where + ``parsed_dict`` maps string keys to parsed values (possibly + :class:`JsonLiteralWithPosition` instances) and ``next_offset`` + is the position of the next unconsumed token. + + Raises: + ValueError: If the token stream does not conform to valid JSON object + syntax (e.g. missing colon, unexpected delimiter, or non-string key). + """ result: dict[Any, Any] = {} while True: begin, _, value, type_ = tokens[offset] @@ -200,7 +231,25 @@ def reparse_object(tokens, offset) -> tuple[dict, int]: def reparse_list(tokens, offset) -> tuple[list, int]: - """Subroutine for handling list parsing inside reparse_value().""" + """Parse a JSON array from the token stream, starting after the opening ``[``. + + Subroutine called by :func:`reparse_value` when an opening square bracket is + encountered. Consumes tokens until the matching closing ``]`` is found. + + Args: + tokens: Token stream as produced by ``tokenize_json()``. + offset (int): Token offset immediately after the opening ``[`` delimiter. + + Returns: + tuple[list, int]: A tuple of ``(parsed_list, next_offset)`` where + ``parsed_list`` contains the parsed elements (possibly + :class:`JsonLiteralWithPosition` instances) and ``next_offset`` + is the position of the next unconsumed token. + + Raises: + ValueError: If the token stream does not conform to valid JSON array + syntax (e.g. unexpected delimiter between elements). + """ result: list[Any] = [] while True: begin, _, value, type_ = tokens[offset] @@ -290,6 +339,10 @@ def fetch_path(json_value: Any, path: tuple): Returns: The node at the indicated path. + + Raises: + TypeError: If ``path`` is not a tuple, if a path element is not a string + or integer, or if an intermediate node is not a dict or list. """ if not isinstance(path, tuple): raise TypeError(f"Expected tuple, but received '{type(path)}'") @@ -323,6 +376,10 @@ def replace_path(json_value: Any, path: tuple, new_value: Any) -> Any: Returns: The modified input, or ``new_value`` itself if the root was replaced. + + Raises: + TypeError: If ``path`` is not a tuple, or if any error propagated from + :func:`fetch_path` during path traversal. """ if not isinstance(path, tuple): raise TypeError(f"Expected tuple, but received '{type(path)}'") diff --git a/mellea/formatters/granite/intrinsics/output.py b/mellea/formatters/granite/intrinsics/output.py index 5eb0bafb3..fb3ca76b7 100644 --- a/mellea/formatters/granite/intrinsics/output.py +++ b/mellea/formatters/granite/intrinsics/output.py @@ -39,7 +39,24 @@ class _MappingType(enum.Enum): class TransformationRule(abc.ABC): - """Base class for transformation rules to apply to JSON outputs of intrinsics.""" + """Base class for transformation rules to apply to JSON outputs of intrinsics. + + Args: + config (dict): Configuration of the parent output processor, as parsed YAML. + input_path_expr (list[str | int | None]): Path expression matching all + instances of the field that this rule transforms. Elements can be + strings for object fields, integers for list indices, or ``None`` + for wildcard matches. + + Attributes: + YAML_NAME (str | None): The name used to identify this rule in YAML + configuration files. Subclasses must set this to a non-``None`` string. + config (dict): Configuration of the parent output processor, as parsed YAML. + input_path_expr (list[str | int | None]): Path expression matching all + instances of the field that this rule transforms. Elements can be + strings for object fields, integers for list indices, or ``None`` + for wildcard matches. + """ YAML_NAME: str | None = None """Subclasses should set this to the name of the rule in YAML config files.""" @@ -47,10 +64,13 @@ class TransformationRule(abc.ABC): def __init__(self, config: dict, input_path_expr: list[str | int | None]): """Initialize the TransformationRule. - :param config: Configuration of the parent output processor, as parsed YAML. - :param input_path_expr: Path expression that matches all instances of the field - that this rule transforms. Elements can be strings for object fields, - ints for list indices, or ``None`` for wildcard matches. + Args: + config (dict): Configuration of the parent output processor, as + parsed YAML. + input_path_expr (list[str | int | None]): Path expression that + matches all instances of the field that this rule transforms. + Elements can be strings for object fields, ints for list + indices, or ``None`` for wildcard matches. """ self.config = config self.input_path_expr = input_path_expr @@ -92,7 +112,14 @@ def _matching_paths(self, parsed_json: Any) -> list[tuple]: return [p for p in json_util.all_paths(parsed_json) if self._is_input_path(p)] def rule_name(self) -> str: - """Get the rule name.""" + """Return the YAML name that identifies this transformation rule. + + Returns: + str: The value of ``YAML_NAME`` for this rule subclass. + + Raises: + ValueError: If ``YAML_NAME`` has not been set by the subclass. + """ if self.YAML_NAME is None: raise ValueError(f"Attempted to fetch missing rule name for {type(self)}") return self.YAML_NAME @@ -121,16 +148,23 @@ def apply( logprobs: ChatCompletionLogProbs | None, chat_completion: ChatCompletion | None, ) -> Any: - """Main entry point. - - :param parsed_json: Output of running model results through - :func:`json.loads()`, plus applying zero or more transformation rules. - :param reparsed_json: Output of running the same model results through - :func:`json_util.reparse_json_with_offsets()`. - :param logprobs: Optional logprobs result associated with the original model - output string, or ``None`` of no logprobs were present. - :param chat_completion: The chat completion request that produced this output. - :returns: Transformed copy of ``parsed_json`` after applying this rule. + """Apply this transformation rule to the parsed model output. + + Args: + parsed_json (Any): Output of running model results through + :func:`json.loads()`, plus applying zero or more prior + transformation rules. + reparsed_json (Any): Output of running the same model results + through :func:`json_util.reparse_json_with_offsets()`, + preserving position information on literal values. + logprobs (ChatCompletionLogProbs | None): Optional logprobs result + associated with the original model output string, or ``None`` + if no logprobs were present. + chat_completion (ChatCompletion | None): The chat completion request + that produced this output. Required by some rules. + + Returns: + Any: Transformed copy of ``parsed_json`` after applying this rule. """ paths = self._matching_paths(parsed_json) prepare_output = self._prepare( @@ -260,6 +294,21 @@ class TokenToFloat(InPlaceTransformation): """Transformation rule that decodes token logprobs to a floating point number. The floating point number replaces the original categorical value in the JSON. + + Args: + config (dict): Configuration of the parent output processor, as parsed YAML. + input_path_expr (list[str | int | None]): Path expression matching all + instances of the field that this rule transforms. + categories_to_values (dict[str | int | bool, float] | None): Mapping + from categorical labels to floating-point values. Defaults to + ``None``. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"likelihood"``. + categories_to_values (dict[str | int | bool, float] | None): Mapping + from categorical label strings or values to their corresponding + floating-point numeric values. """ YAML_NAME = "likelihood" @@ -273,9 +322,14 @@ def __init__( ): """Initialize TokenToFloat transformation rule. - :param categories_to_values: Mapping from categorical labels to floating-point - values. - :type categories_to_values: dict[str | int | bool, float] + Args: + config (dict): Configuration of the parent output processor, as + parsed YAML. + input_path_expr (list[str | int | None]): Path expression that + matches all instances of the field that this rule transforms. + categories_to_values (dict[str | int | bool, float] | None): + Mapping from categorical labels to floating-point values. + Defaults to ``None``. """ super().__init__(config, input_path_expr) self.categories_to_values = categories_to_values @@ -452,7 +506,33 @@ def _desplit_sentences( class DecodeSentences(AddFieldsTransformation): - """Transformation rule that decodes sentence refs into begin, end, text tuples.""" + """Transformation rule that decodes sentence refs into begin, end, text tuples. + + Args: + config (dict): Configuration of the parent output processor, as parsed YAML. + input_path_expr (list[str | int | None]): Path expression matching all + instances of the field that this rule transforms. + source (str): Name of the location to look for sentences; must be + ``"last_message"`` or ``"documents"``. + output_names (dict): Mapping from output role name (``"begin"``, + ``"end"``, ``"text"``, ``"document_id"``) to the name of the new + field to add in the result JSON. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"decode_sentences"``. + source (str): Where to look for tagged sentences; either + ``"last_message"`` or ``"documents"``. + begin_name (str | None): Name of the output field that receives the + sentence begin offset, or ``None`` if not configured. + end_name (str | None): Name of the output field that receives the + sentence end offset, or ``None`` if not configured. + text_name (str | None): Name of the output field that receives the + sentence text, or ``None`` if not configured. + document_id_name (str | None): Name of the output field that receives + the document ID (only used when ``source="documents"``), or + ``None`` if not configured. + """ YAML_NAME = "decode_sentences" @@ -466,9 +546,22 @@ def __init__( ): """Initialize DecodeSentences transformation rule. - :param source: Name of the location to look for sentences; can be "last_message" - or "documents". - :param output_names: Names of new result fields to add + Args: + config (dict): Configuration of the parent output processor, as + parsed YAML. + input_path_expr (list[str | int | None]): Path expression that + matches all instances of the field that this rule transforms. + source (str): Name of the location to look for sentences; must be + ``"last_message"`` or ``"documents"``. + output_names (dict): Mapping from output role name (``"begin"``, + ``"end"``, ``"text"``, ``"document_id"``) to the name of the + new field to add in the result JSON. + + Raises: + ValueError: If ``source`` is not ``"last_message"`` or + ``"documents"``, or if an unexpected key is found in + ``output_names``. + TypeError: If ``output_names`` is not a dict. """ super().__init__(config, input_path_expr) @@ -622,6 +715,12 @@ class Explode(InPlaceTransformation): Turn each row in a list of records into zero or more rows by expanding the elements of a list-valued attribute. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"explode"``. + target_field (str): Name of the list-valued field within each record + to expand. """ YAML_NAME = "explode" @@ -683,7 +782,14 @@ def _transform(self, value, path, prepare_output): class DropDuplicates(InPlaceTransformation): - """Remove duplicate records from a list of records.""" + """Remove duplicate records from a list of records. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"drop_duplicates"``. + target_fields (list): Names of fields used to determine whether two + records are considered duplicates. + """ YAML_NAME = "drop_duplicates" @@ -734,6 +840,13 @@ class Project(InPlaceTransformation): Project records down to a specified set of fields. Can also rename the retained fields. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"project"``. + retained_fields (dict): Mapping from original field name to the + (possibly renamed) output field name. Initialized from either a + list of field names (identity mapping) or an explicit mapping. """ YAML_NAME = "project" @@ -777,7 +890,14 @@ def _transform(self, value, path, prepare_output): class Nest(InPlaceTransformation): - """Convert a value within a JSON structure into a record with a single field.""" + """Convert a value within a JSON structure into a record with a single field. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"nest"``. + field_name (str): Name of the single field in the output JSON object + that wraps each matching value. + """ YAML_NAME = "nest" @@ -801,7 +921,32 @@ def _transform(self, value, path, prepare_output): class MergeSpans(InPlaceTransformation): - """Merge adjacent spans into larger spans.""" + """Merge adjacent spans into larger spans. + + Args: + config: Parsed YAML config for IO processing. + input_path_expr: Path expression for the list of records to merge. + group_fields (list): List of fields used for grouping prior to merging + spans. + begin_field (str): Name of the field that holds the begin offset of + spans. + end_field (str): Name of the field that holds the end offset of spans. + text_field (str | None): Optional field containing covered text strings + that should be concatenated when spans are merged. Defaults to + ``None``. + + Attributes: + YAML_NAME (str): YAML configuration key for this rule; always + ``"merge_spans"``. + group_fields (list): Fields used for grouping records before merging + contiguous spans. + begin_field (str): Name of the field that holds the begin offset of + each span. + end_field (str): Name of the field that holds the end offset of each + span. + text_field (str | None): Optional name of the field containing covered + text strings that are concatenated when spans are merged. + """ YAML_NAME = "merge_spans" @@ -817,14 +962,18 @@ def __init__( ): """Initialize MergeSpans transformation rule. - :param config: Parsed YAML config for IO processing - :param input_path_expr: Path expression for the list of records to explode - :param group_fields: List of fields that are used for grouping prior to merge - spans. - :param begin_field: Name of field that holds the begin offset of spans - :param end_field: Name of field that holds the end offset of spans - :param text_field: Optional field containing covered text strings that should - be concatenated when spans are merged. + Args: + config: Parsed YAML config for IO processing. + input_path_expr: Path expression for the list of records to merge. + group_fields (list): List of fields used for grouping prior to + merging spans. + begin_field (str): Name of the field that holds the begin offset of + spans. + end_field (str): Name of the field that holds the end offset of + spans. + text_field (str | None): Optional field containing covered text + strings that should be concatenated when spans are merged. + Defaults to ``None``. """ super().__init__(config, input_path_expr) self._type_check("group_fields", group_fields, list) @@ -1065,6 +1214,20 @@ class IntrinsicsResultProcessor(ChatCompletionResultProcessor): General-purpose chat completion result processor for use with the models that implement intrinsics. Reads parameters of the model's input and output formats from a YAML configuration file and edits the input chat completion appropriately. + + Attributes: + config (dict): Parsed YAML configuration file for the target intrinsic. + rules (list[TransformationRule]): Ordered list of transformation rules + that this processor applies to each choice in a chat completion + response. + + Args: + config_file (str | pathlib.Path | None): Optional path to a YAML + configuration file. Exactly one of ``config_file`` and + ``config_dict`` must be provided. + config_dict (dict | None): Optional pre-parsed YAML configuration dict. + Exactly one of ``config_file`` and ``config_dict`` must be + provided. """ config: dict @@ -1081,8 +1244,13 @@ def __init__( ): """Initialize IntrinsicsResultProcessor. - :param config_file: (optional) Location of YAML configuration file - :param config_dict: (optional) Parsed contents of YAML configuration file + Args: + config_file (str | pathlib.Path | None): Optional path to a YAML + configuration file. Exactly one of ``config_file`` and + ``config_dict`` must be provided. + config_dict (dict | None): Optional pre-parsed YAML configuration + dict. Exactly one of ``config_file`` and ``config_dict`` must + be provided. """ config = make_config_dict(config_file, config_dict) if config is None: diff --git a/mellea/formatters/granite/intrinsics/util.py b/mellea/formatters/granite/intrinsics/util.py index 0d10f7a84..2e4539d1f 100644 --- a/mellea/formatters/granite/intrinsics/util.py +++ b/mellea/formatters/granite/intrinsics/util.py @@ -40,6 +40,11 @@ def make_config_dict( Returns: Validated configuration dict with optional fields set to ``None`` and JSON string fields parsed to Python objects. + + Raises: + ValueError: If both or neither of ``config_file`` and ``config_dict`` are + provided, if a required field is missing, if an unexpected top-level + field is encountered, or if a JSON field cannot be parsed. """ if (config_file is None and config_dict is None) or ( config_file is not None and config_dict is not None @@ -123,6 +128,10 @@ def obtain_lora( Returns: Full path to the local copy of the specified (a)LoRA adapter, suitable for passing to commands that serve the adapter. + + Raises: + ValueError: If the specified intrinsic adapter cannot be found in the + Hugging Face Hub repository at the expected path. """ # Third Party import huggingface_hub diff --git a/mellea/formatters/granite/retrievers/elasticsearch.py b/mellea/formatters/granite/retrievers/elasticsearch.py index 5fed87807..7db53f0cd 100644 --- a/mellea/formatters/granite/retrievers/elasticsearch.py +++ b/mellea/formatters/granite/retrievers/elasticsearch.py @@ -6,13 +6,35 @@ class ElasticsearchRetriever: - """Retriever for documents hosted on an ElasticSearch server.""" + """Retriever for documents hosted on an Elasticsearch server. + + Queries an Elasticsearch index using the ELSER sparse-vector model to + retrieve the top-k matching documents for a given natural language query. + + Attributes: + corpus_name (str): Name of the Elasticsearch index to query. + hosts (str): Full ``url:port`` connection string to the Elasticsearch + server. + kwargs (dict): Additional keyword arguments forwarded to the + ``Elasticsearch`` client constructor. + + Args: + corpus_name (str): Name of the Elasticsearch index to query. + host (str): Full ``url:port`` connection string to the Elasticsearch + server. + **kwargs (Any): Additional keyword arguments forwarded to the + ``Elasticsearch`` client constructor. + """ def __init__(self, corpus_name: str, host: str, **kwargs: Any): """Initialize ElasticsearchRetriever. - :param hosts: Full url:port to the Elasticsearch server. - :param kwargs: Additional kwargs to pass to the Elasticsearch class. + Args: + corpus_name (str): Name of the Elasticsearch index to query. + host (str): Full ``url:port`` connection string to the Elasticsearch + server. + **kwargs (Any): Additional keyword arguments forwarded to the + ``Elasticsearch`` client constructor. """ # Third Party from elasticsearch import Elasticsearch # type: ignore[import-not-found] @@ -29,8 +51,10 @@ def __init__(self, corpus_name: str, host: str, **kwargs: Any): def create_es_body(self, limit, query): """Create a query body for Elasticsearch. - :param limit: Max number of documents to retrieve. - :param query: Query string for retrieving documents. + Args: + limit (int): Maximum number of documents to retrieve. + query (str): Natural language query string used for ELSER + sparse-vector retrieval. """ body = { "size": limit, @@ -50,7 +74,16 @@ def create_es_body(self, limit, query): return body def retrieve(self, query: str, top_k: int = 5) -> list[dict]: - """Run a query and return results.""" + """Run a query against the Elasticsearch index and return top-k results. + + Args: + query (str): Natural language query string to search for. + top_k (int): Maximum number of documents to return. Defaults to ``5``. + + Returns: + list[dict]: List of matching documents, each with keys ``doc_id``, + ``text``, and ``score``. + """ body = self.create_es_body(top_k, query) retriever_results = self.es.search(index=self.corpus_name, body=body) diff --git a/mellea/formatters/granite/retrievers/embeddings.py b/mellea/formatters/granite/retrievers/embeddings.py index 8081c3bf2..02a595ee7 100644 --- a/mellea/formatters/granite/retrievers/embeddings.py +++ b/mellea/formatters/granite/retrievers/embeddings.py @@ -217,7 +217,15 @@ def write_embeddings( class InMemoryRetriever: - """Simple retriever that keeps docs and embeddings in memory.""" + """Simple retriever that keeps docs and embeddings in memory. + + Args: + data_file_or_table: Parquet file of document snippets and embeddings, or an equivalent + in-memory PyArrow Table. Should have columns ``id``, ``begin``, ``end``, ``text``, + and ``embedding``. + embedding_model_name (str): Name of the Sentence Transformers model to use for embeddings. + Must match the model used to compute embeddings in the data file. + """ def __init__( self, diff --git a/mellea/formatters/granite/retrievers/util.py b/mellea/formatters/granite/retrievers/util.py index 5129dd3f8..3a1cf3784 100644 --- a/mellea/formatters/granite/retrievers/util.py +++ b/mellea/formatters/granite/retrievers/util.py @@ -24,6 +24,9 @@ def download_mtrag_corpus(target_dir: str, corpus_name: str) -> pathlib.Path: Returns: Path to the downloaded (or cached) file. + + Raises: + ValueError: If ``corpus_name`` is not one of the supported corpus names. """ corpus_names = ("cloud", "clapnq", "fiqa", "govt") if corpus_name not in corpus_names: @@ -51,6 +54,10 @@ def read_mtrag_corpus(corpus_file: str | pathlib.Path) -> pa.Table: Returns: Documents from the corpus as a PyArrow table, with schema ``["id", "url", "title", "text"]``. + + Raises: + TypeError: If the ID column cannot be identified or if no ``text`` column + is present in the corpus file. """ if not isinstance(corpus_file, pathlib.Path): corpus_file = pathlib.Path(corpus_file) @@ -96,6 +103,11 @@ def download_mtrag_embeddings(embedding_name: str, corpus_name: str, target_dir: or ``"govt"``. target_dir: Location where Parquet files named ``"part_001.parquet"``, ``"part_002.parquet"``, etc. will be written. + + Raises: + ValueError: If ``corpus_name`` is not one of the supported corpus names, or + if no precomputed embeddings are found for the given corpus and embedding + model combination. """ corpus_names = ("cloud", "clapnq", "fiqa", "govt") if corpus_name not in corpus_names: diff --git a/mellea/formatters/template_formatter.py b/mellea/formatters/template_formatter.py index 694d0919c..d37786d3d 100644 --- a/mellea/formatters/template_formatter.py +++ b/mellea/formatters/template_formatter.py @@ -24,7 +24,32 @@ class TemplateFormatter(ChatFormatter): - """Formatter that uses jinja2 templates.""" + """Formatter that uses Jinja2 templates to render components into prompt strings. + + Template discovery walks a configurable ``template_path`` and the built-in + templates directory. Results are optionally cached in a ``SimpleLRUCache`` + for performance. Use this formatter when your backend requires hand-crafted + prompts rather than generic chat-message rendering. + + Args: + model_id (str | ModelIdentifier): Describes the model for which templates will be looked up. + Should match the template directory structure. + template_path (str): An alternate location where templates can be found. + Will be preferred over all other template directories even if a less exact match is found. + Defaults to ``""``. + use_template_cache (bool): When ``True``, caches template lookup results. Set to ``False`` + if you plan to change ``model_id`` or ``template_path`` after construction. + Defaults to ``True``. + + Attributes: + model_id (str | ModelIdentifier): The model identifier used to locate + model-specific templates during rendering. + + Example:: + + formatter = TemplateFormatter(model_id="my-model", template_path="/path/to/templates") + text = formatter.print(my_component) + """ def __init__( self, @@ -33,12 +58,20 @@ def __init__( template_path: str = "", use_template_cache: bool = True, ): - """A TemplateFormatter use jinja2 templates. + """Initialise a ``TemplateFormatter``. Args: - model_id: Describes the model for which templates will be looked up. Should match the template dir structure. - template_path: Specify an alternate location where templates can be found. Will be preferred over all other template dirs even if a less exact match is found. - use_template_cache: Cache the location of the most recent templates so that future lookups don't need to be performed. Set to false if you plan on changing the model_id or template_path after the TemplateFormatter has been created. + model_id (str | ModelIdentifier): Describes the model for which + templates will be looked up. Should match the template + directory structure. + template_path (str): An alternate location where templates can be + found. Will be preferred over all other template directories + even if a less exact match is found. Defaults to ``""``. + use_template_cache (bool): When ``True``, caches the location of + the most recently used templates so that future lookups are + skipped. Set to ``False`` if you plan on changing ``model_id`` + or ``template_path`` after the formatter has been created. + Defaults to ``True``. """ self.model_id = model_id self._template_path: str = template_path @@ -118,7 +151,14 @@ def _stringify( return str(c) def print(self, c: Component | CBlock) -> str: - """Uses a jinja2 template to pretty-print components.""" + """Render a component or code block to a string using a Jinja2 template. + + Args: + c (Component | CBlock): The component or code block to render. + + Returns: + str: The rendered string representation of the component. + """ return self._stringify(c) def _load_template(self, repr: TemplateRepresentation) -> jinja2.Template: diff --git a/mellea/helpers/async_helpers.py b/mellea/helpers/async_helpers.py index b4935ee6f..6edb90048 100644 --- a/mellea/helpers/async_helpers.py +++ b/mellea/helpers/async_helpers.py @@ -84,6 +84,13 @@ class ClientCache: """A simple [LRU](https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_Recently_Used_(LRU)) cache. Used to keep track of clients for backends where the client is tied to a specific event loop. + + Args: + capacity (int): Maximum number of entries to hold before evicting the least recently used. + + Attributes: + capacity (int): Maximum number of entries the cache can hold. + cache (OrderedDict): Ordered dictionary storing the cached key-value pairs in LRU order. """ def __init__(self, capacity: int): diff --git a/mellea/stdlib/components/chat.py b/mellea/stdlib/components/chat.py index ce1fcfdc6..01b0e1cd9 100644 --- a/mellea/stdlib/components/chat.py +++ b/mellea/stdlib/components/chat.py @@ -28,6 +28,21 @@ class Message(Component["Message"]): TODO: we may want to deprecate this Component entirely. The fact that some Component gets rendered as a chat message is `Formatter` miscellania. + + Args: + role (str): The role that this message came from (e.g., ``"user"``, + ``"assistant"``). + content (str): The content of the message. + images (list[ImageBlock] | None): Optional images associated with the + message. + documents (list[Document] | None): Optional documents associated with + the message. + + Attributes: + Role (type): Type alias for the allowed role literals: ``"system"``, + ``"user"``, ``"assistant"``, or ``"tool"``. + role (str): The role associated with this message. + content (str): The plain-text content of this message. """ Role = Literal["system", "user", "assistant", "tool"] @@ -62,7 +77,12 @@ def images(self) -> None | list[str]: return None def parts(self) -> list[Component | CBlock]: - """Returns all of the constituent parts of an Instruction.""" + """Return the constituent parts of this message, including content, documents, and images. + + Returns: + list[Component | CBlock]: A list beginning with the content block, + followed by any attached documents and image blocks. + """ parts: list[Component | CBlock] = [self._content_cblock] if self._docs is not None: parts.extend(self._docs) @@ -171,7 +191,21 @@ def _parse(self, computed: ModelOutputThunk) -> "Message": class ToolMessage(Message): - """Adds the name field for function name.""" + """Adds the name field for function name. + + Args: + role (str): The role of this message; most backends use ``"tool"``. + content (str): The content of the message; should be a stringified + version of ``tool_output``. + tool_output (Any): The output of the tool or function call. + name (str): The name of the tool or function that was called. + args (Mapping[str, Any]): The arguments passed to the tool. + tool (ModelToolCall): The ``ModelToolCall`` representation. + + Attributes: + name (str): The name of the tool or function that was called. + arguments (Mapping[str, Any]): The arguments that were passed to the tool. + """ def __init__( self, @@ -199,7 +233,12 @@ def __init__( self._tool = tool def format_for_llm(self) -> TemplateRepresentation: - """The same representation as Message with a name field added to args.""" + """Return the same representation as ``Message`` with a ``name`` field added to the args dict. + + Returns: + TemplateRepresentation: Template representation including the tool + name alongside the standard message fields. + """ message_repr = super().format_for_llm() args = message_repr.args args["name"] = self.name @@ -222,6 +261,12 @@ def as_chat_history(ctx: Context) -> list[Message]: Returns: List of ``Message`` objects in conversation order. + + Raises: + Exception: If the context history is non-linear and cannot be cast to a + flat list. + AssertionError: If any entry in the context cannot be converted to a + ``Message``. """ def _to_msg(c: CBlock | Component | ModelOutputThunk) -> Message | None: diff --git a/mellea/stdlib/components/docs/document.py b/mellea/stdlib/components/docs/document.py index 21e572c12..9aa64452e 100644 --- a/mellea/stdlib/components/docs/document.py +++ b/mellea/stdlib/components/docs/document.py @@ -11,16 +11,42 @@ # TODO: Add support for passing in docs as model options. class Document(Component[str]): - """Documents should typically be used in a Message object.""" + """A text passage with optional metadata for grounding model inputs. + + Documents are typically attached to a ``Message`` via its ``documents`` + parameter to enable retrieval-augmented generation (RAG) workflows. + + Args: + text (str): The text content of the document. + title (str | None): An optional human-readable title for the document. + doc_id (str | None): An optional unique identifier for the document. + + Attributes: + text (str): The text content of this document. + title (str | None): Human-readable title, or ``None`` if not set. + doc_id (str | None): Unique identifier, or ``None`` if not set. + """ def __init__(self, text: str, title: str | None = None, doc_id: str | None = None): - """Create a document object. Should typically be used as a list in the `_docs` field of Message.""" + """Create a document object. Should typically be used as a list in the ``_docs`` field of ``Message``. + + Args: + text (str): The text content of the document. + title (str | None): An optional human-readable title. + doc_id (str | None): An optional unique identifier. + """ self.text = text self.title = title self.doc_id = doc_id def parts(self) -> list[Component | CBlock]: - """The set of all the constituent parts of the `Component`.""" + """Returns the constituent parts of this document. + + Returns: + list[Component | CBlock]: An empty list by default since the base + ``Document`` class has no constituent parts. Subclasses may override + this method to return meaningful parts. + """ return [] def format_for_llm(self) -> str: diff --git a/mellea/stdlib/components/docs/richdocument.py b/mellea/stdlib/components/docs/richdocument.py index 44444fbd2..bb51b18fd 100644 --- a/mellea/stdlib/components/docs/richdocument.py +++ b/mellea/stdlib/components/docs/richdocument.py @@ -25,28 +25,44 @@ class RichDocument(Component[str]): - """A `RichDocument` is a block of content with an underlying DoclingDocument. + """A ``RichDocument`` is a block of content backed by a ``DoclingDocument``. - It has helper functions for working with the document and extracting parts of it. + Provides helper functions for working with the document and extracting parts + such as tables. Use ``from_document_file`` to convert PDFs or other formats, + and ``save``/``load`` for persistence. + + Args: + doc (DoclingDocument): The underlying Docling document to wrap. """ def __init__(self, doc: DoclingDocument): - """A `RichDocument` is a block of content with an underlying DoclingDocument.""" + """Create a ``RichDocument`` wrapping the provided ``DoclingDocument``. + + Args: + doc (DoclingDocument): The Docling document to wrap. + """ self._doc = doc def parts(self) -> list[Component | CBlock]: - """RichDocument has no parts. + """Return the constituent parts of this document. - In the future, we should allow chunking of DoclingDocuments to correspond to parts(). + Currently always returns an empty list. Future versions may support + chunking the document into constituent parts. + + Returns: + list[Component | CBlock]: Always an empty list. """ # TODO: we could separate a DoclingDocument into chunks and then treat those chunks as parts. # for now, do nothing. return [] def format_for_llm(self) -> TemplateRepresentation | str: - """Return Document content as Markdown. + """Return the document content as a Markdown string. + + No template is needed; the full document is exported to Markdown directly. - No template needed here. + Returns: + TemplateRepresentation | str: The full document rendered as Markdown. """ return self.to_markdown() @@ -55,7 +71,11 @@ def _parse(self, computed: ModelOutputThunk) -> str: return computed.value if computed.value is not None else "" def docling(self) -> DoclingDocument: - """Get the underlying Docling Document.""" + """Return the underlying ``DoclingDocument``. + + Returns: + DoclingDocument: The wrapped Docling document instance. + """ return self._doc def to_markdown(self): @@ -63,18 +83,35 @@ def to_markdown(self): return self._doc.export_to_markdown() def get_tables(self) -> list[Table]: - """Return the `Table`s that are a part of this document.""" + """Return all tables found in this document. + + Returns: + list[Table]: A list of ``Table`` objects extracted from the document. + """ return [Table(x, self.docling()) for x in self.docling().tables] def save(self, filename: str | Path) -> None: - """Save the underlying DoclingDocument for reuse later.""" + """Save the underlying ``DoclingDocument`` to a JSON file for later reuse. + + Args: + filename (str | Path): Destination file path for the serialized + document. + """ if type(filename) is str: filename = Path(filename) self._doc.save_as_json(filename) @classmethod def load(cls, filename: str | Path) -> RichDocument: - """Load a DoclingDocument from a file. The file must already be a DoclingDocument.""" + """Load a ``RichDocument`` from a previously saved ``DoclingDocument`` JSON file. + + Args: + filename (str | Path): Path to a JSON file previously created by + ``RichDocument.save``. + + Returns: + RichDocument: A new ``RichDocument`` wrapping the loaded document. + """ if type(filename) is str: filename = Path(filename) doc_doc = DoclingDocument.load_from_json(filename) @@ -82,7 +119,15 @@ def load(cls, filename: str | Path) -> RichDocument: @classmethod def from_document_file(cls, source: str | Path | DocumentStream) -> RichDocument: - """Process a document with Docling.""" + """Convert a document file to a ``RichDocument`` using Docling. + + Args: + source (str | Path | DocumentStream): Path or stream for the + source document (e.g. a PDF or Markdown file). + + Returns: + RichDocument: A new ``RichDocument`` wrapping the converted document. + """ pipeline_options = PdfPipelineOptions( images_scale=2.0, generate_picture_images=True ) @@ -102,24 +147,41 @@ class TableQuery(Query): Formats the table as Markdown alongside the query string so the LLM receives both the structured table content and the natural-language question. + + Args: + obj (Table): The table to query. + query (str): The natural-language question to ask about the table. """ def __init__(self, obj: Table, query: str) -> None: - """Initializes a new instance of the `TableQuery` class. + """Initializes a new instance of the ``TableQuery`` class. Args: - obj : The table object to which the query applies. - query : The query string. + obj (Table): The table object to which the query applies. + query (str): The query string. """ super().__init__(obj, query) def parts(self) -> list[Component | CBlock]: - """The list of cblocks/components on which TableQuery depends.""" + """Return the constituent parts of this table query. + + Returns: + list[Component | CBlock]: A list containing the wrapped ``Table`` + object. + """ cs: list[Component | CBlock] = [self._obj] return cs def format_for_llm(self) -> TemplateRepresentation: - """Template arguments for Formatter.""" + """Format this table query for the language model. + + Renders the table as Markdown alongside the query string, and forwards + any tools and fields from the table's own representation. + + Returns: + TemplateRepresentation: Template args containing the query string + and the Markdown-rendered table. + """ assert isinstance(self._obj, Table) tbl_repr = self._obj.format_for_llm() assert isinstance(tbl_repr, TemplateRepresentation) @@ -137,24 +199,41 @@ class TableTransform(Transform): Formats the table as Markdown alongside the transformation instruction so the LLM receives both the structured table content and the mutation description. + + Args: + obj (Table): The table to transform. + transformation (str): Natural-language description of the desired mutation. """ def __init__(self, obj: Table, transformation: str) -> None: - """Initializes a new instance of the `TableTransform` class. + """Initializes a new instance of the ``TableTransform`` class. Args: - obj : The table object to which the transform applies. - transformation : The transformation description string. + obj (Table): The table object to which the transform applies. + transformation (str): The transformation description string. """ super().__init__(obj, transformation) def parts(self) -> list[Component | CBlock]: - """The parts for this component.""" + """Return the constituent parts of this table transform. + + Returns: + list[Component | CBlock]: A list containing the wrapped ``Table`` + object. + """ cs: list[Component | CBlock] = [self._obj] return cs def format_for_llm(self) -> TemplateRepresentation: - """Template arguments for Formatter.""" + """Format this table transform for the language model. + + Renders the table as Markdown alongside the transformation description, + and forwards any tools and fields from the table's own representation. + + Returns: + TemplateRepresentation: Template args containing the transformation + description and the Markdown-rendered table. + """ assert isinstance(self._obj, Table) tbl_repr = self._obj.format_for_llm() assert isinstance(tbl_repr, TemplateRepresentation) @@ -171,17 +250,40 @@ def format_for_llm(self) -> TemplateRepresentation: class Table(MObject): - """A `Table` represents a single table within a larger Docling Document.""" + """A ``Table`` represents a single table within a larger Docling Document. + + Args: + ti (TableItem): The Docling ``TableItem`` extracted from the document. + doc (DoclingDocument): The parent ``DoclingDocument``. Passing ``None`` + may cause downstream Docling functions to fail. + """ def __init__(self, ti: TableItem, doc: DoclingDocument): - """If you pass doc=None, the underlying docling functions to extract data from tables may fail due to lack of context and docling deprecations.""" + """Create a ``Table`` wrapping a Docling ``TableItem``. + + Args: + ti (TableItem): The Docling table item to wrap. + doc (DoclingDocument): The parent Docling document. If ``None``, + some Docling extraction functions may fail. + """ super().__init__(query_type=TableQuery, transform_type=TableTransform) self._ti = ti self._doc = doc @classmethod def from_markdown(cls, md: str) -> Table | None: - """Creates a fake document from the markdown and attempts to extract the first table found.""" + """Create a ``Table`` from a Markdown string by round-tripping through Docling. + + Wraps the Markdown in a minimal document, converts it with Docling, and + returns the first table found. + + Args: + md (str): A Markdown string containing at least one table. + + Returns: + Table | None: The first ``Table`` extracted from the Markdown, or + ``None`` if no table could be found. + """ fake_doc = f"# X\n\n{md}\n" bs = io.BytesIO(fake_doc.encode("utf-8")) doc = RichDocument.from_document_file(DocumentStream(name="x.md", stream=bs)) @@ -191,20 +293,42 @@ def from_markdown(cls, md: str) -> Table | None: return None def parts(self): - """The current implementation does not necessarily entail any string re-use, so parts is empty.""" + """Return the constituent parts of this table component. + + The current implementation always returns an empty list because the + table is rendered entirely through ``format_for_llm``. + + Returns: + list[Component | CBlock]: Always an empty list. + """ return [] def to_markdown(self) -> str: - """Get the `Table` as markdown.""" + """Export this table as a Markdown string. + + Returns: + str: The Markdown representation of this table. + """ return self._ti.export_to_markdown(self._doc) def transpose(self) -> Table | None: - """Transposes the table. Will return a new transposed `Table` if successful.""" + """Transpose this table and return the result as a new ``Table``. + + Returns: + Table | None: A new transposed ``Table``, or ``None`` if the + transposed Markdown cannot be parsed back into a ``Table``. + """ t = self._ti.export_to_dataframe().transpose() return Table.from_markdown(t.to_markdown()) def format_for_llm(self) -> TemplateRepresentation | str: - """Return Table representation for Formatter.""" + """Return the table representation for the Formatter. + + Returns: + TemplateRepresentation | str: A ``TemplateRepresentation`` that + renders the table as its Markdown string using a ``{{table}}`` + template. + """ return TemplateRepresentation( args={"table": self.to_markdown()}, obj=self, diff --git a/mellea/stdlib/components/genslot.py b/mellea/stdlib/components/genslot.py index 686e6f063..88c22ce32 100644 --- a/mellea/stdlib/components/genslot.py +++ b/mellea/stdlib/components/genslot.py @@ -32,7 +32,11 @@ class FunctionResponse(BaseModel, Generic[R]): - """Generic base class for function response formats.""" + """Generic base class for function response formats. + + Attributes: + result (R): The value returned by the generative function. + """ result: R = Field(description="The function result") @@ -61,7 +65,13 @@ def create_response_format(func: Callable[..., R]) -> type[FunctionResponse[R]]: class FunctionDict(TypedDict): - """Return Type for a Function Component.""" + """Return Type for a Function Component. + + Attributes: + name (str): The function's ``__name__``. + signature (str): The function's parameter signature as a string. + docstring (str | None): The function's docstring, or ``None`` if absent. + """ name: str signature: str @@ -69,7 +79,13 @@ class FunctionDict(TypedDict): class ArgumentDict(TypedDict): - """Return Type for a Argument Component.""" + """Return Type for an Argument Component. + + Attributes: + name (str | None): The parameter name. + annotation (str | None): The parameter's type annotation as a string. + value (str | None): The bound value for this parameter as a string. + """ name: str | None annotation: str | None @@ -77,7 +93,13 @@ class ArgumentDict(TypedDict): class Argument: - """A single function argument with its name, type annotation, and value.""" + """A single function argument with its name, type annotation, and value. + + Args: + annotation (str | None): The parameter's type annotation as a string. + name (str | None): The parameter name. + value (str | None): The bound value for this parameter as a string. + """ def __init__( self, @@ -85,7 +107,13 @@ def __init__( name: str | None = None, value: str | None = None, ): - """An Argument.""" + """Create an Argument with optional name, annotation, and value. + + Args: + annotation (str | None): The parameter's type annotation as a string. + name (str | None): The parameter name. + value (str | None): The bound value for this parameter as a string. + """ self._argument_dict: ArgumentDict = { "name": name, "annotation": annotation, @@ -98,10 +126,18 @@ class Arguments(CBlock): Each argument is formatted as ``"- name: value (type: annotation)"`` and the items are newline-joined into a single string suitable for inclusion in a prompt. + + Args: + arguments (list[Argument]): The list of bound function arguments to render. """ def __init__(self, arguments: list[Argument]): - """Create a textual representation of a list of arguments.""" + """Create a textual representation of a list of arguments. + + Args: + arguments (list[Argument]): The list of bound function arguments to + render as a formatted string. + """ # Make meta the original list of arguments and create a list of textual representations. meta: dict[str, Any] = {} text_args = [] @@ -116,10 +152,23 @@ def __init__(self, arguments: list[Argument]): class ArgPreconditionRequirement(Requirement): - """Specific requirement with template for validating precondition requirements against a set of args.""" + """Specific requirement with template for validating precondition requirements against a set of args. + + Args: + req (Requirement): The underlying requirement to wrap. All method calls + are delegated to this requirement. + + Attributes: + req (Requirement): The wrapped underlying requirement instance. + """ def __init__(self, req: Requirement): - """Can only be instantiated from existing requirements. All function calls are delegated to the underlying requirement.""" + """Can only be instantiated from existing requirements. All function calls are delegated to the underlying requirement. + + Args: + req (Requirement): The requirement to wrap for argument precondition + validation. + """ self.req = req def __getattr__(self, name): @@ -133,7 +182,17 @@ def __deepcopy__(self, memo): class PreconditionException(Exception): - """Exception raised when validation fails for a generative slot's arguments.""" + """Exception raised when validation fails for a generative slot's arguments. + + Args: + message (str): Human-readable description of the failure. + validation_results (list[ValidationResult]): The individual validation + results from the failed precondition checks. + + Attributes: + validation (list[ValidationResult]): The validation results from the + failed precondition checks. + """ def __init__( self, message: str, validation_results: list[ValidationResult] @@ -141,8 +200,9 @@ def __init__( """Exception raised when validation fails for a generative slot's arguments. Args: - message: the error message - validation_results: the list of validation results from the failed preconditions + message (str): The error message describing the precondition failure. + validation_results (list[ValidationResult]): The list of validation + results from the failed preconditions. """ super().__init__(message) self.validation = validation_results @@ -154,10 +214,17 @@ class Function(Generic[P, R]): Stores the original callable alongside its name, signature, and docstring as produced by ``describe_function``, so generative slots can render them into prompts without re-inspecting the function each time. + + Args: + func (Callable): The callable to wrap and introspect. """ def __init__(self, func: Callable[P, R]): - """Wrap a callable and capture its metadata via ``describe_function``.""" + """Wrap a callable and capture its metadata via ``describe_function``. + + Args: + func (Callable[P, R]): The callable to wrap. + """ self._func: Callable[P, R] = func self._function_dict: FunctionDict = describe_function(func) @@ -216,6 +283,10 @@ def bind_function_arguments( Returns: Dictionary mapping parameter names to bound values with defaults applied. + + Raises: + TypeError: If required parameters from the original function are missing + from the provided arguments. """ signature = inspect.signature(func) try: @@ -238,6 +309,12 @@ class ExtractedArgs: """Used to extract the mellea args and original function args. See @generative decorator for additional notes on these fields. These args must match those allowed by any overload of GenerativeSlot.__call__. + + Attributes: + f_args (tuple[Any, ...]): Positional args from the original function call; + used to detect incorrectly passed args to generative slots. + f_kwargs (dict[str, Any]): Keyword args intended for the original function. + m (MelleaSession | None): The active Mellea session, if provided. """ f_args: tuple[Any, ...] @@ -276,6 +353,15 @@ class GenerativeSlot(Component[R], Generic[P, R]): ``__call__`` for synchronous and asynchronous invocation respectively. The function's signature, docstring, and type hints are rendered into a prompt so the LLM can imitate the function's intended behaviour. + + Args: + func (Callable): The function whose behaviour the LLM should imitate. + + Attributes: + precondition_requirements (list[Requirement]): Requirements validated + against the function's input arguments before generation. + requirements (list[Requirement]): Requirements validated against the + LLM's generated output. """ def __init__(self, func: Callable[P, R]): @@ -315,14 +401,23 @@ def __call__(self, *args, **kwargs) -> tuple[R, Context] | R: @staticmethod def extract_args_and_kwargs(*args, **kwargs) -> ExtractedArgs: - """Takes a mix of args and kwargs for both the generative slot and the original function and extracts them. Ensures the original function's args are all kwargs. + """Take a mix of args and kwargs for both the generative slot and the original function and extract them. Ensures the original function's args are all kwargs. + + Args: + args: Positional arguments; the first must be either a + ``MelleaSession`` or a ``Context`` instance. + kwargs: Keyword arguments for both the generative slot machinery + (e.g. ``m``, ``context``, ``backend``, ``requirements``) and the + wrapped function's own parameters. Returns: - ExtractedArgs: a dataclass of the required args for mellea and the original function. - Either session or (backend, context) will be non-None. + ExtractedArgs: A dataclass of the required args for mellea and the + original function. Either session or (backend, context) will be + non-None. Raises: - TypeError: if any of the original function's parameters were passed as positional args + TypeError: If any of the original function's parameters were passed + as positional args or if required mellea parameters are missing. """ def _session_extract_args_and_kwargs( @@ -404,7 +499,14 @@ def _context_backend_extract_args_and_kwargs( return extracted def parts(self) -> list[Component | CBlock]: - """Parts of Genslot.""" + """Return the constituent parts of this generative slot component. + + Includes the rendered arguments block (if arguments have been bound) + and any requirements attached to this slot. + + Returns: + list[Component | CBlock]: List of argument blocks and requirements. + """ cs: list = [] if self._arguments is not None: cs.append(self._arguments) @@ -412,7 +514,16 @@ def parts(self) -> list[Component | CBlock]: return cs def format_for_llm(self) -> TemplateRepresentation: - """Formats the instruction for Formatter use.""" + """Format this generative slot for the language model. + + Builds a ``TemplateRepresentation`` containing the function metadata + (name, signature, docstring), the bound arguments, and any requirement + descriptions. + + Returns: + TemplateRepresentation: The formatted representation ready for the + ``Formatter`` to render into a prompt. + """ return TemplateRepresentation( obj=self, args={ diff --git a/mellea/stdlib/components/instruction.py b/mellea/stdlib/components/instruction.py index db03293f0..656c1b39f 100644 --- a/mellea/stdlib/components/instruction.py +++ b/mellea/stdlib/components/instruction.py @@ -27,7 +27,25 @@ class Instruction(Component[str]): - """The Instruction in an instruct/validate/repair loop.""" + """The Instruction in an instruct/validate/repair loop. + + Args: + description (str | CBlock | None): The task description shown to the model. + requirements (list[Requirement | str] | None): Constraints the output must satisfy. + icl_examples (list[str | CBlock] | None): In-context-learning examples. + grounding_context (dict[str, str | CBlock | Component] | None): Named context + passages injected into the prompt. + user_variables (dict[str, str] | None): Jinja2 variable substitutions applied + to all string parameters. + prefix (str | CBlock | None): A prefix prepended before the model's generation. + output_prefix (str | CBlock | None): A prefix prepended to the model's output token + stream (currently unsupported; must be ``None``). + images (list[ImageBlock] | None): Images to include in the prompt. + + Attributes: + requirements (list[Requirement]): The resolved list of requirement instances + attached to this instruction. + """ def __init__( self, @@ -151,7 +169,13 @@ def parts(self): return filtered def format_for_llm(self) -> TemplateRepresentation: - """Formats the instruction for Formatter use.""" + """Format this instruction for the language model. + + Returns: + TemplateRepresentation: A template representation containing the + description, requirements, in-context examples, grounding context, + and optional prefix/repair fields. + """ return TemplateRepresentation( obj=self, args={ @@ -178,7 +202,16 @@ def format_for_llm(self) -> TemplateRepresentation: @staticmethod def apply_user_dict_from_jinja(user_dict: dict[str, str], s: str) -> str: - """Treats s as a jinja string and user_dict as the template values dictionary.""" + """Render a Jinja2 template string using the provided variable dictionary. + + Args: + user_dict (dict[str, str]): Mapping of Jinja2 variable names to their + string replacement values. + s (str): A string treated as a Jinja2 template to be rendered. + + Returns: + str: The rendered string with all Jinja2 placeholders substituted. + """ assert s is not None return jinja2.Template(s).render(user_dict) @@ -188,7 +221,16 @@ def requirements(self) -> list[Requirement]: return self._requirements def copy_and_repair(self, repair_string: str) -> Instruction: - """Creates a copy of the instruction and adds/overwrites the repair string.""" + """Create a deep copy of this instruction with the repair string set. + + Args: + repair_string (str): The repair feedback string to attach, typically + describing which requirements failed and why. + + Returns: + Instruction: A new ``Instruction`` identical to this one but with + ``_repair_string`` set to ``repair_string``. + """ res = deepcopy(self) res._repair_string = repair_string return res diff --git a/mellea/stdlib/components/intrinsic/intrinsic.py b/mellea/stdlib/components/intrinsic/intrinsic.py index d8cc1e032..751d0fb85 100644 --- a/mellea/stdlib/components/intrinsic/intrinsic.py +++ b/mellea/stdlib/components/intrinsic/intrinsic.py @@ -12,7 +12,25 @@ class Intrinsic(Component[str]): - """A component representing an intrinsic.""" + """A component representing an intrinsic fine-tuned adapter capability. + + Intrinsics transform a chat completion request by injecting messages, + modifying model parameters, or applying structured output constraints. + Must be paired with a backend that supports adapter loading. + + Args: + intrinsic_name (str): The user-visible name of the intrinsic; must match + a known name in Mellea's intrinsics catalog. + intrinsic_kwargs (dict | None): Optional keyword arguments required by + the intrinsic at invocation time. + + Attributes: + metadata: The intrinsic metadata fetched from the catalog. + intrinsic_kwargs (dict): Keyword arguments passed to the intrinsic. + intrinsic_name (str): User-visible name of this intrinsic (property). + adapter_types (tuple[AdapterType, ...]): Available adapter types that + implement this intrinsic (property). + """ def __init__( self, intrinsic_name: str, intrinsic_kwargs: dict | None = None @@ -28,10 +46,10 @@ def __init__( An intrinsic component should correspond to a loaded adapter. Args: - intrinsic_name: the user-visible name of the intrinsic; must match a known - name in Mellea's intrinsics catalog. - intrinsic_kwargs: some intrinsics require kwargs when utilizing them; - provide them here + intrinsic_name (str): The user-visible name of the intrinsic; must match + a known name in Mellea's intrinsics catalog. + intrinsic_kwargs (dict | None): Some intrinsics require kwargs when + utilizing them; provide them here. """ self.metadata = fetch_intrinsic_metadata(intrinsic_name) if intrinsic_kwargs is None: @@ -49,19 +67,29 @@ def adapter_types(self) -> tuple[AdapterType, ...]: return self.metadata.adapter_types def parts(self) -> list[Component | CBlock]: - """The set of all the constituent parts of the `Intrinsic`. + """Return the constituent parts of this intrinsic component. - Will need to be implemented by subclasses since not all intrinsics are output - as text / messages. + Will need to be implemented by subclasses since not all intrinsics + produce text or message output. + + Returns: + list[Component | CBlock]: Always an empty list for the base class. """ return [] # TODO revisit this. def format_for_llm(self) -> TemplateRepresentation | str: - """`Intrinsic` doesn't implement `format_for_default`. + """Not implemented for the base ``Intrinsic`` class. + + ``Intrinsic`` components are intended to be used as the *action* passed + directly to the backend, not as a part of the context rendered by the + formatter. - Formats the `Intrinsic` into a `TemplateRepresentation` or string. + Returns: + TemplateRepresentation | str: Never returns; always raises. - Returns: a `TemplateRepresentation` or string + Raises: + NotImplementedError: Always, because ``Intrinsic`` does not + implement ``format_for_llm`` by default. """ raise NotImplementedError( "`Intrinsic` doesn't implement format_for_llm by default. You should only " diff --git a/mellea/stdlib/components/mify.py b/mellea/stdlib/components/mify.py index 01516dfa7..26aa6da3a 100644 --- a/mellea/stdlib/components/mify.py +++ b/mellea/stdlib/components/mify.py @@ -44,40 +44,56 @@ class MifiedProtocol(MObjectProtocol, Protocol): _stringify_func: Callable[[object], str] | None = None def parts(self) -> list[Component | CBlock]: - """TODO: we need to rewrite this component to use format_for_llm and initializer correctly. + """Return the constituent sub-components of this mified object. + + TODO: we need to rewrite this component to use format_for_llm and initializer correctly. For now an empty list is the correct behavior. [no-index] + + Returns: + list[Component | CBlock]: Always an empty list for mified objects. """ return [] def get_query_object(self, query: str) -> Query: - """Returns the instantiated query object. + """Return the instantiated query object for this mified object. [no-index] Args: - query : The query string. + query (str): The natural-language query string. + + Returns: + Query: A ``Query`` component wrapping this object and the given query. """ return self._query_type(self, query) def get_transform_object(self, transformation: str) -> Transform: - """Returns the instantiated transform object. + """Return the instantiated transform object for this mified object. [no-index] Args: - transformation: the transform string + transformation (str): The natural-language transformation description. + + Returns: + Transform: A ``Transform`` component wrapping this object and the + given transformation description. """ return self._transform_type(self, transformation) def content_as_string(self) -> str: - """Returns the content of the Mified object as a string. + """Return the content of the mified object as a plain string. [no-index] - Will use the passed in stringify function if provided. + Delegates to the ``stringify_func`` passed to ``mify`` when one was + provided; otherwise falls back to ``str(self)``. + + Returns: + str: String representation of the mified object's content. """ if self._stringify_func: return self._stringify_func() # type: ignore @@ -177,14 +193,18 @@ def _get_all_fields(self) -> dict[str, Any]: return narrowed def format_for_llm(self) -> TemplateRepresentation: - """The representation of an object given to the backend. + """Return the ``TemplateRepresentation`` for this mified object. [no-index] - Sets the TemplateRepresentation fields based on the object and the values - specified during mify. + Sets the ``TemplateRepresentation`` fields based on the object and the + configuration values supplied to ``mify`` (fields, templates, tools, etc.). + + See the ``mify`` decorator for more details. - See mify decorator for more details. + Returns: + TemplateRepresentation: The formatted representation including args, + tools, and template ordering. """ args = {"content": self.content_as_string()} @@ -219,9 +239,22 @@ def _parse(self, computed: ModelOutputThunk) -> str: return computed.value if computed.value is not None else "" def parse(self, computed: ModelOutputThunk) -> str: - """Parse the model output. Returns string value for now. + """Parse the model output into a string value. [no-index] + + Delegates to ``_parse`` and wraps any exception in a + ``ComponentParseError`` to give callers a consistent error type. + + Args: + computed (ModelOutputThunk): The raw model output to parse. + + Returns: + str: The string value extracted from ``computed``. + + Raises: + ComponentParseError: If ``_parse`` raises any exception during + parsing. """ try: return self._parse(computed) @@ -282,17 +315,25 @@ def mify(*args, **kwargs): # noqa: D417 the attributes and methods of the object/class necessary for it to be mified. Args: - obj: either a class or an instance of the class - fields_include: fields of the object to include in its representation to models - fields_exclude: fields of the object to exclude from its representation to models - funcs_include: functions of the object to include in its representation to models - funcs_exclude: functions of the object to exclude from its representation to models - query_type: a specific query component type to use when querying a model - transform_type: a specific transform component type to use when transforming with a model - template: a string representation of a jinja template; takes precedence over template_order - template_order: a template ordering to use when searching for applicable templates - parsing_func: not yet implemented - stringify_func: used to create a string representation of the object + obj: A class or an instance of a class to mify. Omit when using as a + decorator with arguments (e.g. ``@mify(fields_include={...})``). + query_type: A specific query component type to use when querying a model. + Defaults to ``Query``. + transform_type: A specific transform component type to use when + transforming with a model. Defaults to ``Transform``. + fields_include: Fields of the object to include in its representation to + models. When set, ``stringify_func`` is not used. + fields_exclude: Fields of the object to exclude from its representation + to models. + funcs_include: Functions of the object to expose as tools to models. + funcs_exclude: Functions of the object to hide from models. + template: A Jinja2 template string. Takes precedence over + ``template_order`` when provided. + template_order: A template name or list of names used when searching for + applicable templates. + parsing_func: Not yet implemented. + stringify_func: A callable used to create a string representation of the + object for ``content_as_string``. Returns: An object if an object was passed in or a decorator (callable) to mify classes. diff --git a/mellea/stdlib/components/mobject.py b/mellea/stdlib/components/mobject.py index d8e10c119..0965026c8 100644 --- a/mellea/stdlib/components/mobject.py +++ b/mellea/stdlib/components/mobject.py @@ -24,24 +24,38 @@ class Query(Component[str]): Wraps the object and its query string into a ``TemplateRepresentation`` so the formatter can render both together in a prompt, optionally forwarding the object's tools and fields to the template. + + Args: + obj (Component): The object to be queried. + query (str): The natural-language question to ask about the object. """ def __init__(self, obj: Component, query: str) -> None: """Initializes a new instance of Query with the provided object and query. Args: - obj : The object to be queried. - query: The query string used for querying the object. + obj (Component): The object to be queried. + query (str): The query string used for querying the object. """ self._obj = obj self._query = query def parts(self) -> list[Component | CBlock]: - """Get the parts of the query.""" + """Return the constituent parts of this query component. + + Returns: + list[Component | CBlock]: A list containing the wrapped object. + """ return [self._obj] def format_for_llm(self) -> TemplateRepresentation | str: - """Format the query for llm.""" + """Format this query for the language model. + + Returns: + TemplateRepresentation | str: A ``TemplateRepresentation`` containing + the query string, the wrapped object, and any tools or fields from the + object's own representation. + """ object_repr = self._obj.format_for_llm() return TemplateRepresentation( args={ @@ -73,24 +87,38 @@ class Transform(Component[str]): Wraps the object and its transformation description into a ``TemplateRepresentation`` so the formatter can render both together in a prompt, optionally forwarding the object's tools and fields to the template. + + Args: + obj (Component): The object to be transformed. + transformation (str): The natural-language description of the transformation. """ def __init__(self, obj: Component, transformation: str) -> None: """Initializes a new instance of Transform with the provided object and transformation description. Args: - obj : The object to be queried. - transformation: The string used for transforming the object. + obj (Component): The object to be transformed. + transformation (str): The string describing the desired transformation. """ self._obj = obj self._transformation = transformation def parts(self) -> list[Component | CBlock]: - """Get the parts of the transform.""" + """Return the constituent parts of this transform component. + + Returns: + list[Component | CBlock]: A list containing the wrapped object. + """ return [self._obj] def format_for_llm(self) -> TemplateRepresentation | str: - """Format the transform for llm.""" + """Format this transform for the language model. + + Returns: + TemplateRepresentation | str: A ``TemplateRepresentation`` containing + the transformation description, the wrapped object, and any tools or + fields from the object's own representation. + """ object_repr = self._obj.format_for_llm() return TemplateRepresentation( args={ @@ -121,46 +149,65 @@ class MObjectProtocol(Protocol): """Protocol to describe the necessary functionality of a MObject. Implementers should prefer inheriting from MObject than MObjectProtocol.""" def parts(self) -> list[Component | CBlock]: - """Returns a list of parts for MObject.""" + """Return a list of parts for this MObject. + + Returns: + list[Component | CBlock]: The constituent sub-components. + """ ... def get_query_object(self, query: str) -> Query: - """Returns the instantiated query object. + """Return the instantiated query object. Args: - query : The query string. + query (str): The query string. + + Returns: + Query: A ``Query`` component wrapping this object and the given + query string. """ ... def get_transform_object(self, transformation: str) -> Transform: - """Returns the instantiated transform object. + """Return the instantiated transform object. Args: - transformation: the transform string + transformation (str): The transformation description string. + + Returns: + Transform: A ``Transform`` component wrapping this object and the + given transformation description. """ ... def content_as_string(self) -> str: - """Returns the content of MObject as a string. + """Return the content of this MObject as a plain string. - The default value is just `str(self)`. + The default value is just ``str(self)``. Subclasses should override this method. + + Returns: + str: String representation of this object's content. """ ... def _get_all_members(self) -> dict[str, Callable]: - """Returns a list of all methods from the MObject except methods of the super class. + """Return all methods from this MObject that are not inherited from the superclass. - Undocumented and methods with [no-index] in doc string are ignored. + Undocumented methods and methods with ``[no-index]`` in their docstring + are ignored. """ ... def format_for_llm(self) -> TemplateRepresentation | str: - """The template representation used by the formatter. + """Return the template representation used by the formatter. - The default `TemplateRepresentation` uses an automatic - parsing for tools and fields. The content is retrieved - from `content_as_string()`. + The default ``TemplateRepresentation`` uses automatic parsing for tools + and fields. Content is retrieved from ``content_as_string()``. + + Returns: + TemplateRepresentation | str: The formatted representation for the + language model. """ ... @@ -170,7 +217,14 @@ def _parse(self, computed: ModelOutputThunk) -> str: class MObject(Component[str]): - """An extension of `Component` for adding query and transform operations.""" + """An extension of ``Component`` for adding query and transform operations. + + Args: + query_type (type): The ``Query`` subclass to use when constructing query + components. Defaults to ``Query``. + transform_type (type): The ``Transform`` subclass to use when constructing + transform components. Defaults to ``Transform``. + """ def __init__( self, *, query_type: type = Query, transform_type: type = Transform @@ -178,44 +232,61 @@ def __init__( """Initializes a new instance of MObject with a specified query type and transformation type. Args: - query_type : The type of query to be used, defaults to Query if not provided. - transform_type : The type of transform to be used, defaults to Transform if not provided. + query_type (type): The type of query to be used. Defaults to ``Query``. + transform_type (type): The type of transform to be used. Defaults to + ``Transform``. """ self._query_type = query_type self._transform_type = transform_type def parts(self) -> list[Component | CBlock]: - """MObject has no parts because of how format_for_llm is defined.""" + """MObject has no parts because of how format_for_llm is defined. + + Returns: + list[Component | CBlock]: Always an empty list. + """ return [] def get_query_object(self, query: str) -> Query: - """Returns the instantiated query object. + """Return the instantiated query object. Args: - query : The query string. + query (str): The query string. + + Returns: + Query: A ``Query`` component wrapping this object and the given + query string. """ return self._query_type(self, query) def get_transform_object(self, transformation: str) -> Transform: - """Returns the instantiated transform object. + """Return the instantiated transform object. Args: - transformation: the transform string + transformation (str): The transformation description string. + + Returns: + Transform: A ``Transform`` component wrapping this object and the + given transformation description. """ return self._transform_type(self, transformation) def content_as_string(self) -> str: - """Returns the content of MObject as a string. + """Return the content of this MObject as a plain string. - The default value is just `str(self)`. + The default value is just ``str(self)``. Subclasses should override this method. + + Returns: + str: String representation of this object's content. """ return str(self) def _get_all_members(self) -> dict[str, Callable]: - """Returns a list of all methods from the MObject except methods of the super class. + """Return all methods from this MObject except methods of the superclass. - Undocumented and methods with [no-index] in doc string are ignored. + Undocumented methods and methods with ``[no-index]`` in their docstring + are ignored. """ all_members: dict[str, Callable] = dict( inspect.getmembers(self, predicate=inspect.ismethod) @@ -236,11 +307,14 @@ def _get_all_members(self) -> dict[str, Callable]: return unique_members def format_for_llm(self) -> TemplateRepresentation | str: - """The template representation used by the formatter. + """Return the template representation used by the formatter. + + The default ``TemplateRepresentation`` uses automatic parsing for tools + and fields. Content is retrieved from ``content_as_string()``. - The default `TemplateRepresentation` uses an automatic - parsing for tools and fields. The content is retrieved - from `content_as_string()`. + Returns: + TemplateRepresentation | str: The formatted representation for the + language model. """ tools = { k: MelleaTool.from_callable(c) for k, c in self._get_all_members().items() diff --git a/mellea/stdlib/components/react.py b/mellea/stdlib/components/react.py index caa28a466..b1b6e63b5 100644 --- a/mellea/stdlib/components/react.py +++ b/mellea/stdlib/components/react.py @@ -33,21 +33,35 @@ def _mellea_finalize_tool(answer: str) -> str: class ReactInitiator(Component[str]): - """`ReactInitiator` is used at the start of the ReACT loop to prime the model.""" + """`ReactInitiator` is used at the start of the ReACT loop to prime the model. + + Args: + goal (str): The objective of the react loop. + tools (list[AbstractMelleaTool] | None): Tools available to the agent. + ``None`` is treated as an empty list. + + Attributes: + goal (CBlock): The objective of the react loop wrapped as a content block. + tools (list[AbstractMelleaTool]): The tools made available to the react agent. + """ def __init__(self, goal: str, tools: list[AbstractMelleaTool] | None): """`ReactInitiator` is used at the start of the ReACT loop to prime the model. Args: - goal: the objective of the react loop - tools: a list of tools that are available to the react agent - format: the format/schema of the expected answer + goal (str): The objective of the react loop. + tools (list[AbstractMelleaTool] | None): A list of tools that are + available to the react agent; ``None`` is treated as an empty list. """ self.goal = CBlock(goal) self.tools = tools or [] def parts(self) -> list[Component | CBlock]: - """The set of all the constituent parts of the `Component`.""" + """Return the constituent parts of this component. + + Returns: + list[Component | CBlock]: A list containing the goal content block. + """ return [self.goal] def format_for_llm(self) -> TemplateRepresentation: @@ -89,7 +103,13 @@ def __init__(self): """ReactThought signals that a thinking step should be done.""" def parts(self) -> list[Component | CBlock]: - """Component has no parts.""" + """Return the constituent parts of this component. + + ``ReactThought`` has no sub-components; it solely triggers a thinking step. + + Returns: + list[Component | CBlock]: Always an empty list. + """ return [] def _parse(self, computed: ModelOutputThunk) -> str: diff --git a/mellea/stdlib/components/simple.py b/mellea/stdlib/components/simple.py index 191e7c515..522034075 100644 --- a/mellea/stdlib/components/simple.py +++ b/mellea/stdlib/components/simple.py @@ -37,14 +37,34 @@ def _kwargs_type_check(self, kwargs: dict[str, Any]) -> bool: @staticmethod def make_simple_string(kwargs: dict[str, Any]) -> str: - """Uses <|key|>value to represent a simple component.""" + """Render keyword arguments as ``<|key|>value`` tagged strings. + + Args: + kwargs (dict[str, Any]): Mapping of span names to their ``CBlock`` or + ``Component`` values. + + Returns: + str: Newline-joined tagged representation of all keyword arguments. + """ return "\n".join( [f"<|{key}|>{value}" for (key, value) in kwargs.items()] ) @staticmethod def make_json_string(kwargs: dict[str, Any]) -> str: - """Uses json.""" + """Serialize keyword arguments to a JSON string. + + Each value is converted to its string representation: ``CBlock`` and + ``ModelOutputThunk`` values use their ``.value`` attribute, while + ``Component`` values use ``format_for_llm()``. + + Args: + kwargs (dict[str, Any]): Mapping of span names to ``CBlock``, ``Component``, + or ``ModelOutputThunk`` values. + + Returns: + str: JSON-encoded representation of the keyword arguments. + """ str_args = dict() for key in kwargs.keys(): match kwargs[key]: @@ -57,7 +77,13 @@ def make_json_string(kwargs: dict[str, Any]) -> str: return json.dumps(str_args) def format_for_llm(self) -> str: - """Uses a string rep.""" + """Format this component as a JSON string representation for the language model. + + Delegates to ``make_json_string`` using the stored keyword arguments. + + Returns: + str: JSON-encoded string of all named spans in this component. + """ return SimpleComponent.make_json_string(self._kwargs) def _parse(self, computed: ModelOutputThunk) -> str: diff --git a/mellea/stdlib/components/unit_test_eval.py b/mellea/stdlib/components/unit_test_eval.py index 20e7f9250..a22d50947 100644 --- a/mellea/stdlib/components/unit_test_eval.py +++ b/mellea/stdlib/components/unit_test_eval.py @@ -10,14 +10,26 @@ class Message(BaseModel): - """Schema for a message in the test data.""" + """Schema for a message in the test data. + + Attributes: + role (str): The role of the message sender (e.g. ``"user"`` or + ``"assistant"``). + content (str): The text content of the message. + """ role: str content: str class Example(BaseModel): - """Schema for an example in the test data.""" + """Schema for an example in the test data. + + Attributes: + input (list[Message]): The input messages for this example. + targets (list[Message]): The expected target messages for scoring. + input_id (str): An optional identifier for this input example. + """ input: list[Message] targets: list[Message] = Field(default_factory=list) @@ -25,7 +37,15 @@ class Example(BaseModel): class TestData(BaseModel): - """Schema for test data loaded from json.""" + """Schema for test data loaded from json. + + Attributes: + source (str): Origin identifier for this test dataset. + name (str): Human-readable name for this test dataset. + instructions (str): Evaluation guidelines used by the judge model. + examples (list[Example]): The individual input/target example pairs. + id (str): Unique identifier for this test dataset. + """ source: str name: str @@ -36,14 +56,41 @@ class TestData(BaseModel): @field_validator("examples") @classmethod def validate_examples(cls, v: list[Example]) -> list[Example]: - """Ensure examples list is not empty.""" + """Validate that the examples list is not empty. + + Args: + v (list[Example]): The value of the ``examples`` field being + validated. + + Returns: + list[Example]: The validated examples list, unchanged. + + Raises: + ValueError: If the examples list is empty. + """ if not v: raise ValueError("examples list cannot be empty") return v class TestBasedEval(Component[str]): - """Each TestBasedEval represents a single unit test.""" + """Each TestBasedEval represents a single unit test. + + Args: + source (str): Origin identifier for this test dataset. + name (str): Human-readable name for this test. + instructions (str): Evaluation guidelines used by the judge model. + inputs (list[str]): The input texts for each example. + targets (list[list[str]] | None): Expected target strings for each + input. ``None`` is treated as an empty list. + test_id (str | None): Optional unique identifier for this test. + input_ids (list[str] | None): Optional identifiers for each input. + + Attributes: + source (str): Origin identifier for this test dataset. + name (str): Human-readable name for this test. + instructions (str): Evaluation guidelines used by the judge model. + """ def __init__( self, @@ -55,7 +102,18 @@ def __init__( test_id: str | None = None, input_ids: list[str] | None = None, ): - """Initialize TestBasedEval (for a single unit test).""" + """Initialize TestBasedEval (for a single unit test). + + Args: + source (str): Origin identifier for this test dataset. + name (str): Human-readable name for this test. + instructions (str): Evaluation guidelines used by the judge model. + inputs (list[str]): The input texts for each example. + targets (list[list[str]] | None): Expected target strings for each + input. ``None`` is treated as an empty list. + test_id (str | None): Optional unique identifier for this test. + input_ids (list[str] | None): Optional identifiers for each input. + """ self.source = source self.name = name self.instructions = instructions @@ -65,11 +123,23 @@ def __init__( self.input_ids = input_ids or [] def parts(self) -> list[Component | CBlock]: - """The set of constituent parts of the Component.""" + """Return the constituent parts of this component. + + Returns: + list[Component | CBlock]: Always an empty list; the component + renders entirely via ``format_for_llm``. + """ return [] def format_for_llm(self) -> TemplateRepresentation: - """Formats the test for judge evaluation.""" + """Format this test for judge evaluation. + + Returns: + TemplateRepresentation: A template representation containing the + judge context (input, prediction, target, guidelines) set by + ``set_judge_context``, or an empty args dict if no context has + been set yet. + """ return TemplateRepresentation( obj=self, args=self._judge_context if hasattr(self, "_judge_context") else {}, @@ -83,7 +153,14 @@ def _parse(self, computed: ModelOutputThunk) -> str: def set_judge_context( self, input_text: str, prediction: str, targets_for_input: list[str] ) -> None: - """Set context for judge evaluation.""" + """Set the context dictionary used when formatting this test for judge evaluation. + + Args: + input_text (str): The original input text shown to the model. + prediction (str): The model's generated output to evaluate. + targets_for_input (list[str]): Reference target strings for this + input. An empty list results in ``"N/A"`` as the target text. + """ if len(targets_for_input) == 0: # no reference target_text = "N/A" elif len(targets_for_input) == 1: @@ -102,7 +179,20 @@ def set_judge_context( @classmethod def from_json_file(cls, filepath: str) -> list["TestBasedEval"]: - """Load test evaluations from json/jsonl file, return list of TestBasedEval instances, one per 'unit test'.""" + """Load test evaluations from a JSON file, returning one ``TestBasedEval`` per unit test. + + Args: + filepath (str): Path to a JSON file containing one test-data object + or a JSON array of test-data objects. + + Returns: + list[TestBasedEval]: A list of ``TestBasedEval`` instances, one for + each object found in the file. + + Raises: + ValueError: If any test-data object in the file does not conform to + the ``TestData`` schema. + """ path = Path(filepath) with path.open("r") as f: diff --git a/mellea/stdlib/context.py b/mellea/stdlib/context.py index fc55605e3..b1ed669a3 100644 --- a/mellea/stdlib/context.py +++ b/mellea/stdlib/context.py @@ -14,21 +14,49 @@ class ChatContext(Context): - """Initializes a chat context with unbounded window_size and is_chat=True by default.""" + """Initializes a chat context with unbounded window_size and is_chat=True by default. + + Args: + window_size (int | None): Maximum number of context turns to include when + calling ``view_for_generation``. ``None`` (the default) means the full + history is always returned. + """ def __init__(self, *, window_size: int | None = None): - """Constructs a new chat context.""" + """Constructs a new chat context. + + Args: + window_size (int | None): Maximum number of context turns to include when + calling ``view_for_generation``. ``None`` means unlimited history. + """ super().__init__() self._window_size = window_size def add(self, c: Component | CBlock) -> ChatContext: - """Add a new component/cblock to the context. Returns the new context.""" + """Add a new component or CBlock to the context and return the updated context. + + Args: + c (Component | CBlock): The component or content block to append. + + Returns: + ChatContext: A new ``ChatContext`` with the added entry, preserving the + current ``window_size`` setting. + """ new = ChatContext.from_previous(self, c) new._window_size = self._window_size return new def view_for_generation(self) -> list[Component | CBlock] | None: - """Returns the context in a linearized form. Uses the window_size set during initialization.""" + """Return the context entries to pass to the model, respecting the configured window. + + Uses the ``window_size`` set during initialisation to limit how many past + turns are included. ``None`` is returned when the underlying history is + non-linear. + + Returns: + list[Component | CBlock] | None: Ordered list of context entries up to + ``window_size`` turns, or ``None`` if the history is non-linear. + """ return self.as_list(self._window_size) @@ -36,9 +64,24 @@ class SimpleContext(Context): """A `SimpleContext` is a context in which each interaction is a separate and independent turn. The history of all previous turns is NOT saved..""" def add(self, c: Component | CBlock) -> SimpleContext: - """Add a new component/cblock to the context. Returns the new context.""" + """Add a new component or CBlock to the context and return the updated context. + + Args: + c (Component | CBlock): The component or content block to record. + + Returns: + SimpleContext: A new ``SimpleContext`` containing only the added entry; + prior history is not retained. + """ return SimpleContext.from_previous(self, c) def view_for_generation(self) -> list[Component | CBlock] | None: - """Returns an empty list.""" + """Return an empty list, since ``SimpleContext`` does not pass history to the model. + + Each call to the model is treated as a stateless, independent exchange. + No prior turns are forwarded. + + Returns: + list[Component | CBlock] | None: Always an empty list. + """ return [] diff --git a/mellea/stdlib/requirements/python_reqs.py b/mellea/stdlib/requirements/python_reqs.py index c5dd916f2..623ab863c 100644 --- a/mellea/stdlib/requirements/python_reqs.py +++ b/mellea/stdlib/requirements/python_reqs.py @@ -140,7 +140,24 @@ def _python_executes_without_error( class PythonExecutionReq(Requirement): - """Verifies that Python code runs without raising exceptions.""" + """Verifies that Python code runs without raising exceptions. + + Extracts the highest-scoring Python code block from the model's last output + and validates or executes it according to the configured execution mode. + + Args: + timeout (int): Maximum seconds to allow code to run. Defaults to ``5``. + allow_unsafe_execution (bool): If ``True``, execute code directly with + subprocess. Use only with trusted sources. + allowed_imports (list[str] | None): Allowlist of importable top-level + modules. ``None`` allows any import. + use_sandbox (bool): If ``True``, use ``llm-sandbox`` for Docker-based + isolated execution. + + Attributes: + validation_fn (Callable[[Context], ValidationResult]): The validation + function attached to this requirement; always non-``None``. + """ def __init__( self, diff --git a/mellea/stdlib/requirements/requirement.py b/mellea/stdlib/requirements/requirement.py index 3db2a8b41..ded1ca248 100644 --- a/mellea/stdlib/requirements/requirement.py +++ b/mellea/stdlib/requirements/requirement.py @@ -9,7 +9,12 @@ class LLMaJRequirement(Requirement): - """A requirement that always uses LLM-as-a-Judge. Any available constraint ALoRA will be ignored.""" + """A requirement that always uses LLM-as-a-Judge. Any available constraint ALoRA will be ignored. + + Attributes: + use_aloras (bool): Always ``False`` for this class; ALoRA adapters are + never used even if they are available. + """ use_aloras: bool = False @@ -44,7 +49,20 @@ def requirement_check_to_bool(x: CBlock | str) -> bool: class ALoraRequirement(Requirement, Intrinsic): - """A requirement that always uses an (possibly specified) ALora. If an exception is thrown during the ALora execution path, `mellea` will fall back to LLMaJ. But that is the only case where LLMaJ will be used.""" + """A requirement validated by an ALoRA adapter; falls back to LLM-as-a-Judge only on error. + + If an exception is thrown during the ALoRA execution path, ``mellea`` will + fall back to LLMaJ. That is the only case where LLMaJ will be used. + + Args: + description (str): Human-readable requirement description. + intrinsic_name (str | None): Name of the ALoRA intrinsic to use. + Defaults to ``"requirement_check"``. + + Attributes: + use_aloras (bool): Always ``True``; this class always attempts to use + ALoRA adapters for validation. + """ def __init__(self, description: str, intrinsic_name: str | None = None): """A requirement that is validated by an ALora. @@ -71,7 +89,7 @@ def __init__(self, description: str, intrinsic_name: str | None = None): def reqify(r: str | Requirement) -> Requirement: - """Maps strings to Requirements. + """Map strings to Requirements. This is a utility method for functions that allow you to pass in Requirements as either explicit Requirement objects or strings that you intend to be interpreted as requirements. @@ -80,6 +98,9 @@ def reqify(r: str | Requirement) -> Requirement: Returns: A ``Requirement`` instance. + + Raises: + Exception: If ``r`` is neither a ``str`` nor a ``Requirement`` instance. """ if type(r) is str: return Requirement(r) @@ -148,6 +169,10 @@ def simple_validate( Returns: A validation function that takes a ``Context`` and returns a ``ValidationResult``. + + Raises: + ValueError: If ``fn`` returns a type other than ``bool`` or + ``tuple[bool, str]``. """ def validate(ctx: Context) -> ValidationResult: diff --git a/mellea/stdlib/requirements/safety/guardian.py b/mellea/stdlib/requirements/safety/guardian.py index 4e840f46f..141786dc4 100644 --- a/mellea/stdlib/requirements/safety/guardian.py +++ b/mellea/stdlib/requirements/safety/guardian.py @@ -21,6 +21,18 @@ class GuardianRisk(Enum): """Risk definitions for Granite Guardian models. Based on https://github.com/ibm-granite/granite-guardian but updated for 3.3 8B support. + + Attributes: + HARM (str): General harmful content risk. + GROUNDEDNESS (str): Factual groundedness / attribution risk. + PROFANITY (str): Profane language risk. + ANSWER_RELEVANCE (str): Answer relevance to the question risk. + JAILBREAK (str): Jailbreak attempt risk. + FUNCTION_CALL (str): Unsafe or invalid function-call risk. + SOCIAL_BIAS (str): Social bias risk. + VIOLENCE (str): Violent content risk. + SEXUAL_CONTENT (str): Explicit sexual content risk. + UNETHICAL_BEHAVIOR (str): Unethical behaviour risk. """ HARM = "harm" @@ -36,7 +48,11 @@ class GuardianRisk(Enum): @classmethod def get_available_risks(cls) -> list[str]: - """Get list of all available risk types.""" + """Return a list of all available risk type identifiers. + + Returns: + list[str]: String values of all ``GuardianRisk`` enum members. + """ return [risk.value for risk in cls] @@ -71,6 +87,24 @@ class GuardianCheck(Requirement): """Enhanced risk checking using Granite Guardian 3.3 8B with multiple backend support. [DEPRECATED as of V 0.4 -- Use Intrinsics instead] + + Args: + risk (str | GuardianRisk | None): The type of risk to check for. Required + unless ``custom_criteria`` is provided. + backend_type (BackendType): Backend type to use -- ``"ollama"`` or + ``"huggingface"``. + model_version (str | None): Specific Guardian model version. Defaults to + the appropriate 8B model for the chosen backend. + device (str | None): Device string for HuggingFace inference (e.g. + ``"cuda"``). + ollama_url (str): Base URL for the Ollama server. + thinking (bool): Enable chain-of-thought reasoning mode in the Guardian model. + custom_criteria (str | None): Free-text criteria string used in place of a + standard ``GuardianRisk`` value. + context_text (str | None): Context document for groundedness checks. + tools (list[dict] | None): Tool schemas for function-call validation. + backend (Backend | None): Pre-initialised backend instance to reuse; avoids + loading the model multiple times. """ def __init__( @@ -179,12 +213,23 @@ def __init__( self._logger = FancyLogger.get_logger() def get_effective_risk(self) -> str: - """Get the effective risk criteria to use for validation.""" + """Return the effective risk criteria to use for validation. + + Returns the ``custom_criteria`` string when one was provided, otherwise + returns the ``risk`` identifier set during initialisation. + + Returns: + str: The active risk/criteria string forwarded to the Guardian model. + """ return self._custom_criteria if self._custom_criteria else self._risk @classmethod def get_available_risks(cls) -> list[str]: - """Get list of all available standard risk types.""" + """Return a list of all available standard risk type identifiers. + + Returns: + list[str]: String values of all ``GuardianRisk`` enum members. + """ return GuardianRisk.get_available_risks() def __deepcopy__(self, memo): @@ -215,7 +260,25 @@ async def validate( format: type[BaseModelSubclass] | None = None, model_options: dict | None = None, ) -> ValidationResult: - """Validate conversation using Granite Guardian via selected backend.""" + """Validate a conversation using Granite Guardian via the selected backend. + + Builds a minimal chat context from the current session context, invokes the + Guardian model, and parses its ``yes/no`` output. A ``"No"`` + label (risk not detected) is treated as a passing validation result. + + Args: + backend (Backend): The session backend (used as a fallback context + source; the Guardian's own backend is used for generation). + ctx (Context): The current conversation context to validate. + format (type[BaseModelSubclass] | None): Unused; present for interface + compatibility. + model_options (dict | None): Additional model options merged into the + Guardian backend call. + + Returns: + ValidationResult: ``result=True`` when the content is considered safe + (Guardian returns ``"No"``), ``result=False`` otherwise. + """ logger = self._logger # Build a fresh chat context for the guardian model (keep it minimal). diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index ed49e5632..ea7cd0941 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -40,7 +40,20 @@ class BaseSamplingStrategy(SamplingStrategy): - """Base class for multiple strategies that rejects samples based on given instructions.""" + """Base class for multiple strategies that reject samples based on given instructions. + + Args: + loop_budget (int): Maximum number of generate/validate cycles. Must be + greater than 0. Defaults to ``1``. + requirements (list[Requirement] | None): Global requirements evaluated + on every sample. When set, overrides per-call requirements. + + Attributes: + loop_budget (int): Maximum number of generate/validate cycles before + falling back to ``select_from_failure``. + requirements (list[Requirement] | None): Global requirements evaluated + on every sample; takes precedence over per-call requirements when set. + """ loop_budget: int diff --git a/mellea/stdlib/sampling/budget_forcing.py b/mellea/stdlib/sampling/budget_forcing.py index 6515fb8e8..3fb10247a 100644 --- a/mellea/stdlib/sampling/budget_forcing.py +++ b/mellea/stdlib/sampling/budget_forcing.py @@ -32,6 +32,40 @@ class BudgetForcingSamplingStrategy(RejectionSamplingStrategy): accept or retry. Currently only supports the ``OllamaModelBackend``. + + Args: + think_max_tokens (int | None): Tokens allocated for the thinking block. + Defaults to ``4096``. + answer_max_tokens (int | None): Tokens allocated for the answer block. + ``None`` means unbounded. + start_think_token (str | None): Token opening the thinking block. + Defaults to ``""``. + end_think_token (str | None): Token closing the thinking block. + Defaults to ``""``. + begin_response_token (str | None): Optional token opening the response + block. Defaults to ``""``. + end_response_token (str): Token closing the response block. + think_more_suffix (str | None): Suffix to force continued thinking. + Empty string disables forcing. + answer_suffix (str | None): Suffix to elicit a final answer. + loop_budget (int): Rejection-sampling loop count. Must be > 0. + requirements (list[Requirement] | None): Requirements to validate against. + + Attributes: + think_max_tokens (int | None): Maximum tokens allocated for the thinking + block. ``None`` means no budget (zero-think case). + answer_max_tokens (int | None): Maximum tokens allocated for the answer + block. ``None`` means unbounded (generate until EoS). + start_think_token (str | None): Token that opens the thinking block, + e.g. ``""``. + end_think_token (str | None): Token that closes the thinking block, + e.g. ``""``. + begin_response_token (str | None): Optional token that opens the + response block, e.g. ``""``. + end_response_token (str): Token that closes the response block. + think_more_suffix (str | None): Suffix appended to force continued + thinking. Empty string disables forced thinking. + answer_suffix (str | None): Suffix appended to elicit a final answer. """ think_max_tokens: int | None diff --git a/mellea/stdlib/sampling/majority_voting.py b/mellea/stdlib/sampling/majority_voting.py index d770d6526..48e50e198 100644 --- a/mellea/stdlib/sampling/majority_voting.py +++ b/mellea/stdlib/sampling/majority_voting.py @@ -20,7 +20,24 @@ class BaseMBRDSampling(RejectionSamplingStrategy): - """Abstract Minimum Bayes Risk Decoding (MBRD) Sampling Strategy.""" + """Abstract Minimum Bayes Risk Decoding (MBRD) Sampling Strategy. + + Args: + number_of_samples (int): Number of samples to generate and use for + majority voting. Defaults to ``8``. + weighted (bool): Not yet implemented. If ``True``, weights scores + before majority vote. + loop_budget (int): Inner rejection-sampling loop count. Must be > 0. + requirements (list[Requirement] | None): Requirements to validate + against. If ``None``, uses per-call requirements. + + Attributes: + number_of_samples (int): Number of candidate samples to draw and compare. + weighted (bool): Whether to apply per-sample weights before voting. + Currently not implemented; reserved for future use. + symmetric (bool): Whether the similarity metric is symmetric, allowing + the upper-triangle score matrix to be mirrored. + """ number_of_samples: int weighted: bool @@ -57,10 +74,32 @@ def __init__( @abc.abstractmethod def compare_strings(self, ref: str, pred: str) -> float: - """This method is the abstract method for MBRD similarity metric.""" + """Compute a similarity score between a reference and a predicted string. - def maybe_apply_weighted(self, scr: np.ndarray): - """Applies weights if self.weighted is True. Not Implemented.""" + Subclasses must implement this to define the MBRD similarity metric. + + Args: + ref (str): The reference string to compare against. + pred (str): The predicted string to evaluate. + + Returns: + float: A similarity score, typically in ``[0.0, 1.0]`` where ``1.0`` + indicates a perfect match. + """ + + def maybe_apply_weighted(self, scr: np.ndarray) -> np.ndarray: + """Apply per-sample weights to the score vector if ``self.weighted`` is ``True``. + + Currently not implemented; the input array is returned unchanged when + ``self.weighted`` is ``True``. + + Args: + scr (np.ndarray): 1-D array of aggregated similarity scores, one + entry per candidate sample. + + Returns: + np.ndarray: The (possibly weighted) score array. + """ # TODO: not implemented yet if self.weighted: weights = np.asarray([1.0 for _ in range(len(scr))]) @@ -162,7 +201,29 @@ async def sample( class MajorityVotingStrategyForMath(BaseMBRDSampling): - """MajorityVoting Sampling Strategy for Math Expressions.""" + """MajorityVoting Sampling Strategy for Math Expressions. + + Args: + number_of_samples (int): Number of samples to generate. Defaults to ``8``. + float_rounding (int): Decimal places for float comparison. Defaults to ``6``. + strict (bool): Enforce strict comparison mode. Defaults to ``True``. + allow_set_relation_comp (bool): Allow set-relation comparisons. Defaults + to ``False``. + weighted (bool): Not yet implemented. Defaults to ``False``. + loop_budget (int): Rejection-sampling loop count. Defaults to ``1``. + requirements (list[Requirement] | None): Requirements to validate against. + + Attributes: + number_of_samples (int): Number of candidate samples to generate. + match_types (list[str]): Extraction target types used for parsing math + expressions (e.g. ``["latex", "axpr"]``). + float_rounding (int): Number of decimal places used when comparing + floating-point results. + strict (bool): Whether strict comparison mode is enforced (variables + matter; sets are not comparable with tuples). + allow_set_relation_comp (bool): Whether set-relation comparisons are + permitted in all cases. + """ number_of_samples: int match_types: list[str] @@ -223,7 +284,20 @@ def __init__( # https://github.com/huggingface/Math-Verify/blob/5d148cfaaf99214c2e4ffb4bc497ab042c592a7a/tests/test_all.py#L36 def compare_strings(self, ref: str, pred: str) -> float: - """Helper function to compare strings using the math extraction metrics.""" + """Compare two strings using math-aware extraction and verification. + + Parses both strings into mathematical expressions using the configured + ``match_types`` (latex and/or expr), then verifies equivalence via + ``math_verify.verify``. + + Args: + ref (str): The reference (gold) string containing a math expression. + pred (str): The predicted string to compare against the reference. + + Returns: + float: ``1.0`` if the expressions are considered equivalent, + ``0.0`` otherwise. + """ # Convert string match_types to ExtractionTarget objects extraction_targets = [] for match_type in self.match_types: @@ -248,7 +322,21 @@ def compare_strings(self, ref: str, pred: str) -> float: class MBRDRougeLStrategy(BaseMBRDSampling): - """Sampling Strategy that uses RougeL to compute symbol-level distances for majority voting.""" + """Sampling Strategy that uses RougeL to compute symbol-level distances for majority voting. + + Args: + number_of_samples (int): Number of samples to generate. Defaults to ``8``. + weighted (bool): Not yet implemented. Defaults to ``False``. + loop_budget (int): Rejection-sampling loop count. Defaults to ``1``. + requirements (list[Requirement] | None): Requirements to validate against. + + Attributes: + match_types (list[str]): Rouge metric names used for scoring (``["rougeL"]``). + scorer (RougeScorer): Pre-configured ``RougeScorer`` instance used for + pairwise string comparison. + symmetric (bool): Inherited from ``BaseMBRDSampling``; always ``True`` for + RougeL (the score is symmetric by construction). + """ match_types: list[str] scorer: RougeScorer @@ -287,6 +375,14 @@ def __init__( # https://github.com/huggingface/Math-Verify/blob/5d148cfaaf99214c2e4ffb4bc497ab042c592a7a/tests/test_all.py#L36 def compare_strings(self, ref: str, pred: str) -> float: - """Helper function to compare strings using the math extraction metrics.""" + """Compare two strings using the RougeL F-measure. + + Args: + ref (str): The reference string to score against. + pred (str): The predicted string to evaluate. + + Returns: + float: RougeL F-measure score in the range ``[0.0, 1.0]``. + """ scr: float = self.scorer.score(ref, pred)[self.match_types[-1]].fmeasure return scr diff --git a/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py b/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py index b3fc63db9..7d211e386 100644 --- a/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py +++ b/mellea/stdlib/sampling/sampling_algos/budget_forcing_alg.py @@ -66,7 +66,11 @@ async def think_budget_forcing( model_options: Any model options to upsert into the defaults for this call. Returns: - A ``ModelOutputThunk`` containing the assembled thinking and answer response. + ModelOutputThunk: The assembled thinking and answer response. + + Raises: + Exception: If the backend returns generation results without the + required ``meta`` information (e.g. token usage counts). Assumptions: - The chat template is applied on prompt, with think mode enabled diff --git a/mellea/stdlib/sampling/sofai.py b/mellea/stdlib/sampling/sofai.py index e77654350..c08277f55 100644 --- a/mellea/stdlib/sampling/sofai.py +++ b/mellea/stdlib/sampling/sofai.py @@ -34,14 +34,39 @@ class SOFAISamplingStrategy(SamplingStrategy): - """SOFAI sampling strategy. + """SOFAI (Slow and Fast AI) two-solver sampling strategy. - Uses S1 Solver (fast model) in a loop with targeted feedback from validation results. - If S1 Solver fails after exhausting the budget or shows no improvement, - escalates to a single attempt with S2 Solver (slow model). + Uses S1 Solver (fast model) in a loop with targeted feedback from validation + results. If S1 Solver fails after exhausting the budget or shows no + improvement, escalates to a single attempt with S2 Solver (slow model). - The strategy leverages ValidationResult.reason fields to provide targeted + The strategy leverages ``ValidationResult.reason`` fields to provide targeted feedback for repair, enabling more effective iterative improvement. + + Args: + s1_solver_backend (Backend): Backend for the fast S1 solver used in the + iterative repair loop. + s2_solver_backend (Backend): Backend for the slow S2 solver used as a + final escalation step. + s2_solver_mode (Literal["fresh_start", "continue_chat", "best_attempt"]): + How to invoke the S2 solver when S1 fails. + loop_budget (int): Maximum number of S1 repair attempts before escalating + to S2. Must be greater than 0. Defaults to ``3``. + judge_backend (Backend | None): Optional backend for LLM-as-Judge + validation. If ``None``, falls back to the session backend. + feedback_strategy (Literal["simple", "first_error", "all_errors"]): + Detail level of repair feedback provided to the S1 solver. + + Attributes: + s1_solver_backend (Backend): Backend for the fast S1 solver. + s2_solver_backend (Backend): Backend for the slow S2 solver. + s2_solver_mode (str): How to invoke the S2 solver: ``"fresh_start"``, + ``"continue_chat"``, or ``"best_attempt"``. + loop_budget (int): Maximum S1 attempts before escalating to S2. + judge_backend (Backend | None): Optional third backend for LLM-as-Judge + validation. ``None`` uses the session backend. + feedback_strategy (str): Detail level of feedback: ``"simple"``, + ``"first_error"``, or ``"all_errors"``. """ def __init__( diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index c9f41b5ce..5194a2eab 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -75,6 +75,11 @@ def backend_name_to_class(name: str) -> Any: Returns: The corresponding ``Backend`` class, or ``None`` if the name is unrecognised. + + Raises: + ImportError: If the requested backend has optional dependencies that are + not installed (e.g. ``mellea[hf]``, ``mellea[watsonx]``, or + ``mellea[litellm]``). """ if name == "ollama": from ..backends.ollama import OllamaModelBackend @@ -157,6 +162,12 @@ def start_session( MelleaSession: A session object that can be used as a context manager or called directly with session methods. + Raises: + Exception: If ``backend_name`` is not one of the recognised backend + identifiers. + ImportError: If the requested backend requires optional dependencies + that are not installed. + Examples: ```python # Basic usage with default settings @@ -272,6 +283,17 @@ class MelleaSession: If you are doing complicating programming (e.g., non-trivial inference scaling) then you might be better off forgoing `MelleaSession`s and managing your Context and Backend directly. Note: we put the `instruct`, `validate`, and other convenience functions here instead of in `Context` or `Backend` to avoid import resolution issues. + + Args: + backend (Backend): The backend to use for all model inference in this + session. + ctx (Context | None): The conversation context. Defaults to a new + ``SimpleContext`` if ``None``. + + Attributes: + ctx (Context): The active conversation context. Updated after every call that + produces model output. + backend (Backend): The backend used for model inference throughout the session. """ ctx: Context @@ -347,7 +369,11 @@ def clone(self): return copy(self) def reset(self): - """Reset the context state.""" + """Reset the context state to a fresh, empty context of the same type. + + Replaces ``self.ctx`` with the result of ``ctx.reset_to_new()``, discarding + all accumulated conversation history. + """ if has_plugins(HookType.SESSION_RESET): from ..plugins.hooks.session import SessionResetPayload @@ -358,7 +384,7 @@ def reset(self): self.ctx = self.ctx.reset_to_new() def cleanup(self) -> None: - """Clean up session resources.""" + """Clean up session resources and deregister session-scoped plugins.""" if has_plugins(HookType.SESSION_CLEANUP): from ..plugins.hooks.session import SessionCleanupPayload @@ -376,10 +402,6 @@ def cleanup(self) -> None: deregister_session_plugins(self.id) - self.reset() - if hasattr(self.backend, "close"): - self.backend.close() # type: ignore - @overload def act( self, @@ -1008,7 +1030,16 @@ async def atransform( @classmethod def powerup(cls, powerup_cls: type): - """Appends methods in a class object `powerup_cls` to MelleaSession.""" + """Appends methods in a class object `powerup_cls` to MelleaSession. + + Iterates over all functions defined on ``powerup_cls`` and attaches each + one as a method on the ``MelleaSession`` class, effectively extending + the session with domain-specific helpers at runtime. + + Args: + powerup_cls (type): A class whose functions should be added to + ``MelleaSession`` as instance methods. + """ for name, fn in inspect.getmembers(powerup_cls, predicate=inspect.isfunction): setattr(cls, name, fn) diff --git a/mellea/stdlib/tools/interpreter.py b/mellea/stdlib/tools/interpreter.py index 14aee139f..5554b21ed 100644 --- a/mellea/stdlib/tools/interpreter.py +++ b/mellea/stdlib/tools/interpreter.py @@ -38,6 +38,32 @@ class ExecutionResult: using the `analysis_result` field. TODO: should we also be trying to pass back the value of the final expression evaluated, or the value of locals() and globals()? + + Args: + success (bool): ``True`` if execution succeeded (exit code 0 or + static-analysis passed); ``False`` otherwise. + stdout (str | None): Captured standard output, or ``None`` if + execution was skipped. + stderr (str | None): Captured standard error, or ``None`` if + execution was skipped. + skipped (bool): ``True`` when execution was not attempted. + skip_message (str | None): Explanation of why execution was skipped. + analysis_result (Any | None): Optional payload from static-analysis + environments. + + Attributes: + success (bool): ``True`` if the code executed with exit code 0 or if a + static-analysis check passed; ``False`` otherwise. + stdout (str | None): Standard output captured from the subprocess, or + ``None`` if execution was skipped. + stderr (str | None): Standard error captured from the subprocess, or + ``None`` if execution was skipped. + skipped (bool): ``True`` when execution was not attempted (e.g. due to + unauthorized imports or a missing sandbox dependency). + skip_message (str | None): Human-readable explanation of why execution + was skipped, when ``skipped`` is ``True``. + analysis_result (Any | None): Optional payload returned by static-analysis + environments (e.g. the parsed AST tree or a ``SyntaxError`` instance). """ success: bool @@ -78,26 +104,56 @@ def to_validationresult_reason(self) -> str: class ExecutionEnvironment(ABC): - """Abstract environment for executing Python code.""" + """Abstract environment for executing Python code. + + Args: + allowed_imports (list[str] | None): Allowlist of top-level module names + that generated code may import. ``None`` disables the import check. + + Attributes: + allowed_imports (list[str] | None): Allowlist of top-level module names that + generated code may import. ``None`` disables the import check entirely. + """ def __init__(self, allowed_imports: list[str] | None = None): """Initialize with optional import restrictions. Args: - allowed_imports: List of allowed import modules. None means any import is allowed. + allowed_imports (list[str] | None): List of allowed import modules. + ``None`` means any import is allowed. """ self.allowed_imports = allowed_imports @abstractmethod def execute(self, code: str, timeout: int) -> ExecutionResult: - """Execute code and return result.""" + """Execute the given code and return the result. + + Args: + code (str): The Python source code to execute. + timeout (int): Maximum number of seconds to allow the code to run. + + Returns: + ExecutionResult: Execution outcome including stdout, stderr, and + success flag. + """ class StaticAnalysisEnvironment(ExecutionEnvironment): """Safe environment that validates but does not execute code.""" def execute(self, code: str, timeout: int) -> ExecutionResult: - """Validate code syntax and imports without executing.""" + """Validate code syntax and imports without executing. + + Args: + code (str): The Python source code to validate. + timeout (int): Ignored for static analysis; present for interface + compatibility. + + Returns: + ExecutionResult: Result with ``skipped=True`` and the parsed AST in + ``analysis_result`` on success, or a syntax-error description on + failure. + """ try: parse_tree = ast.parse(code) except SyntaxError as e: @@ -135,7 +191,18 @@ class UnsafeEnvironment(ExecutionEnvironment): """Unsafe environment that executes code directly with subprocess.""" def execute(self, code: str, timeout: int) -> ExecutionResult: - """Execute code with subprocess after checking imports.""" + """Execute code with subprocess after checking imports. + + Args: + code (str): The Python source code to execute. + timeout (int): Maximum number of seconds before the subprocess is + killed and a timeout result is returned. + + Returns: + ExecutionResult: Execution outcome with captured stdout/stderr and + success flag, or a skipped result if imports are unauthorized or an + unexpected error occurs. + """ if self.allowed_imports: unauthorized = _get_unauthorized_imports(code, self.allowed_imports) if unauthorized: @@ -195,7 +262,21 @@ class LLMSandboxEnvironment(ExecutionEnvironment): """Environment using llm-sandbox for secure Docker-based execution.""" def execute(self, code: str, timeout: int) -> ExecutionResult: - """Execute code using llm-sandbox.""" + """Execute code using llm-sandbox in an isolated Docker container. + + Checks the import allowlist first, then delegates to a ``SandboxSession`` + from the ``llm-sandbox`` package. Returns a skipped result if + ``llm-sandbox`` is not installed. + + Args: + code (str): The Python source code to execute. + timeout (int): Maximum number of seconds to allow the sandboxed + process to run. + + Returns: + ExecutionResult: Execution outcome with stdout/stderr and success + flag, or a skipped result on import violation or sandbox error. + """ if self.allowed_imports: unauthorized = _get_unauthorized_imports(code, self.allowed_imports) if unauthorized: From 58f0721eeece0b67319afb796ac5fb4ef6996ef6 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Thu, 12 Mar 2026 11:32:02 +0000 Subject: [PATCH 06/14] docs: remove redundant Attributes: entries and drop TASK.md (#613) Remove Attributes: sections where fields duplicate constructor params verbatim (same name, same type, same description), keeping only entries that add genuinely new information (computed/derived attributes): - mellea/backends/backend.py: FormatterBackend - mellea/core/base.py: CBlock, ImageBlock, ModelOutputThunk, ContextTurn - cli/eval/runner.py: InputEvalResult, TestEvalResult (keep computed properties passed_count, total_count, pass_rate) Also removes TASK.md left over from development. --- TASK.md | 67 -------------------------------------- cli/eval/runner.py | 10 ------ mellea/backends/backend.py | 4 --- mellea/core/base.py | 37 --------------------- mellea/stdlib/session.py | 3 +- 5 files changed, 2 insertions(+), 119 deletions(-) delete mode 100644 TASK.md diff --git a/TASK.md b/TASK.md deleted file mode 100644 index 7689a71d4..000000000 --- a/TASK.md +++ /dev/null @@ -1,67 +0,0 @@ -# Task: Fix Ollama exception propagation in `generate_from_raw` - -## Context - -This branch has partial fixes for issues discovered in #573 (test flakiness on local/M1). -Most test fixes are already done (see git diff). One code fix remains. - -## Remaining work - -### Fix exception propagation in `OllamaModelBackend.generate_from_raw` (`mellea/backends/ollama.py`) - -Currently `asyncio.gather(..., return_exceptions=True)` silently converts any exception into -`ModelOutputThunk(value="")`, storing the error only in `generate_log.extra["error"]` — invisible -to callers. A temporary `FancyLogger.warning()` was added as a diagnostic measure but the -exception is still swallowed. - -**Fix:** remove `return_exceptions=True` so exceptions propagate naturally, consistent with all -other backends. Then remove the `isinstance(response, BaseException)` handling block and the -`FancyLogger.warning()` that was added as a temporary measure. - -This aligns with the approach taken in #580 (fail fast, don't swallow exceptions) and gives -consistent behaviour across all backends. - -## Related issues / PRs - -- #573 — test flakiness (this branch addresses most items) -- #577 — same root cause in streaming path -- #580 — fix for streaming path (`astream()` in `base.py`), now merged -- #432 — original bug report covering all backends; closes when #580 merges - -## What's already done in this branch - -- `researcher.py` — `slow` marker added -- `test_format` — `MAX_NEW_TOKENS` bumped to 1024 (ollama + openai_ollama) -- `test_generate_from_raw_with_format` — stale `xfail` removed, assertions validate all results -- `test_generate_from_raw` — 150s timeout added, tighter assertions, reduced context window (2048) **[caveat: see below]** -- `slow` marker description updated (">1 minute") -- Stale `xfail` removed from `test_litellm_watsonx.py` - -## Caveats / open questions - -### `CONTEXT_WINDOW: 2048` in `test_generate_from_raw` may itself cause failures - -Added to reduce KV cache pressure (32K × 4 parallel = 128K slots → 2K × 4 = 8K). In -theory harmless for simple arithmetic prompts. However a 20-run soak after an extended -test session (machine under sustained load) showed 18/20 failures, with Ollama returning -**empty-body responses** (`response.response == ""`) rather than exceptions or hangs. -The `assert all(r.value for r in results)` assertion was catching these empty responses — -the FancyLogger.warning() did NOT fire (no `BaseException` in the gather results), confirming -the empty string was a legitimate Ollama response, not a caught exception. - -This may be caused by the reduced context window interacting with Ollama's internal state -under load, or simply by machine exhaustion — a cold run after the machine rests is needed -to distinguish. The `CONTEXT_WINDOW: 2048` change should be treated as provisional until -this is resolved. - -### aiohttp unclosed session reference - -TASK.md cites "litellm bug #11657" for the unclosed aiohttp `ClientSession` at suite -teardown — confirm this issue number is correct before referencing it in a PR. - -## Out of scope - -- Unclosed aiohttp `ClientSession` at teardown — blocked on upstream litellm bug #11657 -- `asyncio_default_fixture_loop_scope` pytest-asyncio warning — low priority -- The intermittent Ollama hang under load — still under investigation; fixing exception - propagation will at least make failures visible rather than silent diff --git a/cli/eval/runner.py b/cli/eval/runner.py index 77bb4a3c8..9fdf55ff3 100644 --- a/cli/eval/runner.py +++ b/cli/eval/runner.py @@ -33,12 +33,6 @@ class InputEvalResult: score (int): Numeric score assigned by the judge (``1`` for pass, ``0`` for fail). validation_reason (str): Justification text returned by the judge model. - Attributes: - input_text (str): The raw input text sent to the generation model. - model_output (str): The text response produced by the generation model. - validation_passed (bool): Whether the judge scored this response as passing. - score (int): Numeric score assigned by the judge (``1`` for pass, ``0`` for fail). - validation_reason (str): Justification text returned by the judge model. """ def __init__( @@ -81,10 +75,6 @@ class TestEvalResult: produced by running the generation and judge models. Attributes: - test_eval (TestBasedEval): The unit test specification containing - the test ID, name, instructions, inputs, and expected targets. - input_results (list[InputEvalResult]): Per-input evaluation outcomes - produced by running the generation and judge models. passed_count (int): Number of inputs that received a passing score. total_count (int): Total number of inputs evaluated. pass_rate (float): Fraction of inputs that passed (``passed_count / total_count``). diff --git a/mellea/backends/backend.py b/mellea/backends/backend.py index 3421778be..8040eb825 100644 --- a/mellea/backends/backend.py +++ b/mellea/backends/backend.py @@ -34,10 +34,6 @@ class FormatterBackend(Backend, abc.ABC): model_options (dict | None): Default model options; if ``None``, an empty dict is used. - Attributes: - model_id (str | ModelIdentifier): The model identifier for this backend. - model_options (dict): The default model options for generation requests. - formatter (ChatFormatter): The formatter used to render components into prompts. """ def __init__( diff --git a/mellea/core/base.py b/mellea/core/base.py index 340014913..4cb054ae8 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -40,9 +40,6 @@ class CBlock: completion object). Defaults to an empty dict. cache (bool): If ``True``, the inference engine may store the KV cache for this block. Experimental. - Attributes: - cache (bool): Whether the inference engine may store the KV cache for this block. - value (str | None): The underlying string content of the block. """ def __init__( @@ -93,8 +90,6 @@ class ImageBlock(CBlock): value (str): A valid base64-encoded PNG string (with or without a data URI prefix). meta (dict[str, Any] | None): Optional metadata to associate with this image block. - Attributes: - value (str): The base64-encoded PNG string representing the image. """ def __init__(self, value: str, meta: dict[str, Any] | None = None): @@ -276,10 +271,6 @@ class ModelOutputThunk(CBlock, Generic[S]): parsed_repr (S | None): An already-parsed representation to attach; set when re-wrapping existing output. tool_calls (dict[str, ModelToolCall] | None): Tool calls returned by the model alongside the text output. - Attributes: - parsed_repr (S | None): The parsed representation of the output, non-``None`` once computed. - tool_calls (dict[str, ModelToolCall] | None): Tool calls requested by the model, if any. - value (str | None): The raw string output from the model, or ``None`` if not yet computed. """ def __init__( @@ -596,11 +587,6 @@ class ContextTurn: output (ModelOutputThunk | None): The model's output thunk for this turn, or ``None`` for an input-only partial turn. - Attributes: - model_input (CBlock | Component | None): The input component or content block for this turn, - or ``None`` when the turn represents an output-only partial turn. - output (ModelOutputThunk | None): The model's output thunk for this turn, - or ``None`` when the turn represents an input-only partial turn. """ model_input: CBlock | Component | None @@ -859,15 +845,6 @@ class TemplateRepresentation: template_order (list[str] | None): An optional ordering hint for template sections/keys. images (list[ImageBlock] | None): Optional list of image blocks associated with this representation. - Attributes: - obj (Any): The original component object being represented. - args (dict): Named arguments extracted from the component for template substitution. - tools (dict[str, AbstractMelleaTool] | None): Tools available for this representation, - keyed by the tool's function name. ``None`` if the component has no tools. - fields (list[Any] | None): An optional ordered list of field values for positional templates. - template (str | None): An optional Jinja2 (or similar) template string to use when rendering. - template_order (list[str] | None): An optional ordering hint for template sections/keys. - images (list[ImageBlock] | None): Optional list of image blocks associated with this representation. """ obj: Any @@ -902,16 +879,6 @@ class GenerateLog: is_final_result (bool | None): Whether this log entry corresponds to the definitive final result. extra (dict[str, Any] | None): Arbitrary extra metadata to attach to the log entry. - Attributes: - date (datetime.datetime | None): Timestamp when the generation was logged. - prompt (str | list[dict] | None): The prompt string or chat-message list sent to the model. - backend (str | None): Identifier of the inference backend used for this generation. - model_options (dict[str, Any] | None): Model configuration options applied to this call. - model_output (Any | None): The raw output returned by the backend API. - action (Component | CBlock | None): The component or block that triggered the generation. - result (ModelOutputThunk | None): The ``ModelOutputThunk`` produced by this generation call. - is_final_result (bool | None): Whether this log entry corresponds to the definitive final result. - extra (dict[str, Any] | None): Arbitrary extra metadata to attach to the log entry. """ date: datetime.datetime | None = None @@ -936,10 +903,6 @@ class ModelToolCall: func (AbstractMelleaTool): The ``AbstractMelleaTool`` instance that will be invoked. args (Mapping[str, Any]): The keyword arguments the model supplied for the tool call. - Attributes: - name (str): The name of the tool the model requested to call. - func (AbstractMelleaTool): The ``AbstractMelleaTool`` instance that will be invoked. - args (Mapping[str, Any]): The keyword arguments the model supplied for the tool call. """ name: str diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 5194a2eab..85e4ae83d 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -371,7 +371,8 @@ def clone(self): def reset(self): """Reset the context state to a fresh, empty context of the same type. - Replaces ``self.ctx`` with the result of ``ctx.reset_to_new()``, discarding + Fires the ``SESSION_RESET`` plugin hook if any plugins are registered, then + replaces ``self.ctx`` with the result of ``ctx.reset_to_new()``, discarding all accumulated conversation history. """ if has_plugins(HookType.SESSION_RESET): From abcb4ccffdb966746b43a9e8fc6ab203fecbcb16 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Thu, 12 Mar 2026 12:07:42 +0000 Subject: [PATCH 07/14] chore: upgrade mypy to 1.19.1 to fix cpex import-not-found in CI mypy 1.18.2 reports import-not-found for cpex.framework.* (which has no py.typed marker). 1.19.1 handles this correctly and was the version used when PR #582 passed CI. --- uv.lock | 130 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 102 insertions(+), 28 deletions(-) diff --git a/uv.lock b/uv.lock index e64504fb0..620271694 100644 --- a/uv.lock +++ b/uv.lock @@ -3248,6 +3248,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, ] +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, + { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, + { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, + { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, + { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + [[package]] name = "litellm" version = "1.79.3" @@ -4308,40 +4381,41 @@ wheels = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/5a/0c/7d5300883da16f0063ae53996358758b2a2df2a09c72a5061fa79a1f5006/mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce", size = 12893775, upload-time = "2025-09-19T00:10:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/2cffbf25737bdb236f60c973edf62e3e7b4ee1c25b6878629e88e2cde967/mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d", size = 11936852, upload-time = "2025-09-19T00:10:51.631Z" }, - { url = "https://files.pythonhosted.org/packages/be/50/34059de13dd269227fb4a03be1faee6e2a4b04a2051c82ac0a0b5a773c9a/mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c", size = 12480242, upload-time = "2025-09-19T00:11:07.955Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/040983fad5132d85914c874a2836252bbc57832065548885b5bb5b0d4359/mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb", size = 13326683, upload-time = "2025-09-19T00:09:55.572Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/89b2901dd77414dd7a8c8729985832a5735053be15b744c18e4586e506ef/mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075", size = 13514749, upload-time = "2025-09-19T00:10:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/25/bc/cc98767cffd6b2928ba680f3e5bc969c4152bf7c2d83f92f5a504b92b0eb/mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf", size = 9982959, upload-time = "2025-09-19T00:10:37.344Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] From ba9baa0d5b4d2de5e24d8724d1f3538ee50ecf16 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 11:27:24 +0000 Subject: [PATCH 08/14] docs: fix pipeline docstring - call count depends on constraints not fixed at five --- cli/decompose/pipeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/decompose/pipeline.py b/cli/decompose/pipeline.py index 85344da76..452e16a5b 100644 --- a/cli/decompose/pipeline.py +++ b/cli/decompose/pipeline.py @@ -122,9 +122,10 @@ def decompose( ) -> DecompPipelineResult: """Break a task prompt into structured subtasks using a multi-step LLM pipeline. - Orchestrates five sequential LLM calls to produce a fully structured + Orchestrates a series of sequential LLM calls to produce a fully structured decomposition: subtask listing, constraint extraction, validation strategy selection, prompt template generation, and per-subtask constraint assignment. + The number of calls depends on the number of constraints extracted. Args: task_prompt: Natural-language description of the task to decompose. From ba986df53c1e52abe886f8784cb00b257c8a824c Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 12:06:43 +0000 Subject: [PATCH 09/14] =?UTF-8?q?docs:=20apply=20Option=20C=20docstring=20?= =?UTF-8?q?convention=20=E2=80=94=20Args:=20on=20class=20only,=20clean=20A?= =?UTF-8?q?ttributes:=20(#613)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Args: from __init__ docstrings across 60 classes; Args: moved to (or already present on) the class docstring so the docs pipeline (which skips __init__) and IDE hover both show the full parameter list without duplication - Review Attributes: on 46 classes: remove pure-echo entries that repeat Args: verbatim; retain sections where stored values differ in type or behaviour (type transforms such as str→CBlock, class constants such as YAML_NAME, computed values such as SamplingResult.result_index) - audit_coverage.py: drop no_attributes check (now optional by design); add duplicate_init_args check to catch Args: appearing in both class and __init__ - CONTRIBUTING.md: add class/__init__ docstring placement section with example - AGENTS.md: expand Google-style docstring bullet with Option C rule; remove duplicate line --- AGENTS.md | 3 +- CONTRIBUTING.md | 33 ++++++- mellea/backends/adapters/adapter.py | 35 +------ mellea/backends/backend.py | 8 +- mellea/backends/cache.py | 15 +-- mellea/backends/dummy.py | 11 +-- mellea/backends/huggingface.py | 16 +--- mellea/backends/litellm.py | 11 +-- mellea/backends/model_ids.py | 8 -- mellea/backends/ollama.py | 11 +-- mellea/backends/openai.py | 14 +-- mellea/backends/tools.py | 3 - mellea/backends/vllm.py | 10 +- mellea/backends/watsonx.py | 12 +-- mellea/core/base.py | 23 +---- mellea/core/requirement.py | 28 +----- mellea/core/sampling.py | 24 ++--- mellea/formatters/granite/intrinsics/input.py | 14 +-- .../formatters/granite/intrinsics/output.py | 94 +++---------------- .../granite/retrievers/elasticsearch.py | 15 +-- .../granite/retrievers/embeddings.py | 11 +-- mellea/formatters/template_formatter.py | 20 +--- mellea/helpers/async_helpers.py | 12 +-- mellea/stdlib/components/chat.py | 26 +---- mellea/stdlib/components/docs/document.py | 12 +-- mellea/stdlib/components/docs/richdocument.py | 28 +----- mellea/stdlib/components/genslot.py | 43 ++------- mellea/stdlib/components/instruction.py | 13 +-- .../stdlib/components/intrinsic/intrinsic.py | 17 +--- mellea/stdlib/components/mobject.py | 22 +---- mellea/stdlib/components/react.py | 8 +- mellea/stdlib/components/unit_test_eval.py | 17 +--- mellea/stdlib/context.py | 7 +- mellea/stdlib/requirements/python_reqs.py | 9 +- mellea/stdlib/requirements/requirement.py | 7 +- mellea/stdlib/requirements/safety/guardian.py | 15 +-- mellea/stdlib/sampling/base.py | 13 +-- mellea/stdlib/sampling/budget_forcing.py | 31 +----- mellea/stdlib/sampling/majority_voting.py | 59 ++---------- mellea/stdlib/sampling/sofai.py | 29 +----- mellea/stdlib/session.py | 14 +-- mellea/stdlib/tools/interpreter.py | 23 +---- tooling/docs-autogen/audit_coverage.py | 43 +++++---- 43 files changed, 150 insertions(+), 717 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0396b617e..32adb3d24 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,10 +89,9 @@ Tests/examples automatically skip if system lacks required resources. Heavy exam ## 4. Coding Standards - **Types required** on all core functions - **Docstrings are prompts** — be specific, the LLM reads them -- **Google-style docstrings** +- **Google-style docstrings** — `Args:` on the **class docstring only**; `__init__` gets a single summary sentence. Add `Attributes:` only when a stored value differs in type/behaviour from its constructor input (type transforms, computed values, class constants). See CONTRIBUTING.md for a full example. - **Ruff** for linting/formatting - Use `...` in `@generative` function bodies -- Use `...` in `@generative` function bodies - Prefer primitives over classes - **Friendly Dependency Errors**: Wraps optional backend imports in `try/except ImportError` with a helpful message (e.g., "Please pip install mellea[hf]"). See `mellea/stdlib/session.py` for examples. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea66ac185..ea9019b8c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,14 +132,14 @@ Use **[Google-style docstrings](https://google.github.io/styleguide/pyguide.html ```python def extract_entities(text: str, entity_types: list[str]) -> dict[str, list[str]]: """Extract named entities from text. - + Args: text: The input text to analyze. entity_types: List of entity types to extract (e.g., ["PERSON", "ORG"]). - + Returns: Dictionary mapping entity types to lists of extracted entities. - + Example: >>> extract_entities("Alice works at IBM", ["PERSON", "ORG"]) {"PERSON": ["Alice"], "ORG": ["IBM"]} @@ -147,6 +147,33 @@ def extract_entities(text: str, entity_types: list[str]) -> dict[str, list[str]] ... ``` +#### Class and `__init__` docstrings + +Place `Args:` on the **class docstring only**. The `__init__` docstring should be a +single summary sentence with no `Args:` section. This keeps hover docs clean in IDEs +and ensures the docs pipeline (which skips `__init__`) publishes the full parameter +list. + +```python +class MyComponent(Component[str]): + """A component that does something useful. + + Args: + name (str): Human-readable label for this component. + max_tokens (int): Upper bound on generated tokens. + """ + + def __init__(self, name: str, max_tokens: int = 256) -> None: + """Initialize MyComponent with a name and token budget.""" + self.name = name + self.max_tokens = max_tokens +``` + +Add an `Attributes:` section on the class docstring **only** when a stored attribute +differs in type or behaviour from the constructor input — for example, when a `str` +argument is wrapped into a `CBlock`, or when a class-level constant is relevant to +callers. Pure-echo entries that repeat `Args:` verbatim should be omitted. + ### Code Style - **Ruff** for linting and formatting diff --git a/mellea/backends/adapters/adapter.py b/mellea/backends/adapters/adapter.py index 9acbfa0da..80ac85837 100644 --- a/mellea/backends/adapters/adapter.py +++ b/mellea/backends/adapters/adapter.py @@ -33,8 +33,6 @@ class Adapter(abc.ABC): ``AdapterType.LORA`` or ``AdapterType.ALORA``). Attributes: - name (str): Human-readable name of the adapter. - adapter_type (AdapterType): Adapter type (``LORA`` or ``ALORA``). qualified_name (str): Unique name used for loading and lookup; formed as ``"_"``. backend (Backend | None): The backend this adapter has been added to, @@ -44,15 +42,7 @@ class Adapter(abc.ABC): """ def __init__(self, name: str, adapter_type: AdapterType): - """An adapter that can be added to a backend. - - Note: An adapter can only be added to a single backend. - - Args: - name: name of the adapter; when referencing this adapter, use - adapter.qualified_name - adapter_type: enum describing what type of adapter it is (ie LORA / ALORA) - """ + """Initialize Adapter with a name and adapter type.""" self.name = name self.adapter_type = adapter_type self.qualified_name = name + "_" + adapter_type.value @@ -127,20 +117,7 @@ def __init__( config_dict: dict | None = None, base_model_name: str | None = None, ): - """Entry point for creating IntrinsicAdapter objects. - - An adapter that can be added to either an `OpenAIBackend` or a `LocalHFBackend`. - Most intrinsics support LoRA or aLoRA adapter types. - - Args: - intrinsic_name: name of the intrinsic; the local name of the loaded adapter - that implements this intrinsic will be adapter.qualified_name - adapter_type: enum describing what type of adapter it is (ie LORA / ALORA) - config_file: optional; file for defining the intrinsic / transformations - config_dict: optional; dict for defining the intrinsic / transformations - base_model_name: optional; if provided with no config_file/config_dict, - will be used to look up the IO processing config for this adapter - """ + """Initialize IntrinsicAdapter for the named intrinsic, loading its I/O configuration.""" super().__init__(intrinsic_name, adapter_type) self.intrinsic_name = intrinsic_name @@ -350,13 +327,7 @@ class CustomIntrinsicAdapter(IntrinsicAdapter): def __init__( self, *, model_id: str, intrinsic_name: str | None = None, base_model_name: str ): - """Custom granite adapters. - - Args: - model_id: the huggingface model id. Used for downloading model weights. - intrinsic_name (optional): catalog name. Defaults to the repo name if none is provided. For example, nfulton/stembolts model_id uses an intrinsic name of stembolts. - base_model_name: the name (NOT repo_id) of the model. - """ + """Initialize CustomIntrinsicAdapter and patch the global intrinsics catalog if needed.""" assert re.match(".*/.*", model_id), ( "expected a huggingface model id with format /" ) diff --git a/mellea/backends/backend.py b/mellea/backends/backend.py index 8040eb825..b8329392d 100644 --- a/mellea/backends/backend.py +++ b/mellea/backends/backend.py @@ -43,13 +43,7 @@ def __init__( *, model_options: dict | None = None, ): - """Initializes a formatter-based backend for `model_id`. - - Args: - model_id (str): The model_id to use. - formatter (Formatter): The formatter to use for converting components into (fragments of) prompts. - model_options (Optional[dict]): The model options to use; if None, sensible defaults will be provided. - """ + """Initialize a formatter-based backend for the given model ID.""" self.model_id = model_id self.model_options = model_options if model_options is not None else {} self.formatter: ChatFormatter = formatter diff --git a/mellea/backends/cache.py b/mellea/backends/cache.py index c1c9238fd..81f7a7ab3 100644 --- a/mellea/backends/cache.py +++ b/mellea/backends/cache.py @@ -67,21 +67,12 @@ class SimpleLRUCache(Cache): evicted value whenever an entry is removed to make room for a new one. Attributes: - capacity (int): Maximum number of items this cache will hold. - cache (OrderedDict): Internal ordered dict used for LRU tracking. - on_evict (Callable[[Any], None] | None): Eviction callback, or ``None`` if not set. + cache (OrderedDict): Internal ordered dict used for LRU tracking; always + initialised empty at construction. """ def __init__(self, capacity: int, on_evict: Callable[[Any], None] | None = None): - """Initializes the LRU cache with a certain capacity. - - The `SimpleLRUCache` either contains a value or it doesn't. There is no cache hierarchy. Take care when choosing `capacity`. In practice usually a small value will be fine, but ideally you should try to choose a capacity based upon your available device memory and the context size of your model. - - Args: - capacity: Maximum number of items to store in the cache. - on_evict: Optional callback function called when an item is evicted from the cache. - This can be used to free resources (e.g., GPU memory) when items are removed. - """ + """Initialize the LRU cache with a fixed capacity and optional eviction callback.""" self.capacity = capacity self.cache: OrderedDict = OrderedDict() self.on_evict = on_evict diff --git a/mellea/backends/dummy.py b/mellea/backends/dummy.py index 14136b4a9..81c7ca2c3 100644 --- a/mellea/backends/dummy.py +++ b/mellea/backends/dummy.py @@ -24,17 +24,12 @@ class DummyBackend(Backend): return ``"dummy"``. Attributes: - responses (list[str] | None): The list of predetermined responses, or - ``None`` if the backend always returns ``"dummy"``. - idx (int): Index of the next response to return from ``responses``. + idx (int): Index of the next response to return from ``responses``; + starts at ``0`` and increments on each call. """ def __init__(self, responses: list[str] | None): - """Initializes the dummy backend, optionally with a list of dummy responses. - - Args: - responses: If `None`, then the dummy backend always returns "dummy". Otherwise, returns the next item from responses. The generate function will throw an exception if a generate call is made after the list is exhausted. - """ + """Initialize the dummy backend with an optional list of predetermined responses.""" self.responses = responses self.idx = 0 diff --git a/mellea/backends/huggingface.py b/mellea/backends/huggingface.py index c185ba05d..cdff13aee 100644 --- a/mellea/backends/huggingface.py +++ b/mellea/backends/huggingface.py @@ -238,8 +238,6 @@ class LocalHFBackend(FormatterBackend, AdapterMixin): Mellea ``ModelOption`` sentinel keys. from_mellea_model_opts_map (dict): Mapping from Mellea sentinel keys to HF-specific option names. - default_to_constraint_checking_alora (bool): Whether aLoRA constraint checking - is enabled by default. """ _cached_blocks: dict[str, DynamicCache] = dict() @@ -255,19 +253,7 @@ def __init__( default_to_constraint_checking_alora: bool = True, model_options: dict | None = None, ): - """Attempt to load model weights using the model_id by default, or using `custom_config` if provided. - - WARNING: initializing a `LocalHFBackend` will download and load the model on your *local* machine. - - Args: - model_id (str | ModelIdentifier): Used to load the model *and tokenizer* via transformers Auto* classes, and then moves the model to the best available device (cuda > mps > cpu). If loading the model and/or tokenizer from a string will not work, or if you want to use a different device string, then you can use custom_config. - formatter (Formatter): A mechanism for turning `stdlib` stuff into strings. Experimental Span-based models should use `mellea.backends.span.*` backends. - use_caches (bool): If set to False, then caching will not be used even if a Cache is provided. - cache (Optional[Cache]): The caching strategy to use. If None, `LRUCache(3)` will be used. - custom_config (Optional[TransformersTorchConfig]): Overrides loading from the `model_id`. If set, then the specified tokenizer/model/device will be used instead of auto-loading from the model_id. - default_to_constraint_checking_alora: If set to False then aloras will be deactivated. This is primarily for performance benchmarking and debugging. - model_options (Optional[dict]): Default model options. - """ + """Load model weights from the given model ID, or from a custom config if provided.""" formatter = ( formatter if formatter is not None else TemplateFormatter(model_id=model_id) ) diff --git a/mellea/backends/litellm.py b/mellea/backends/litellm.py index 26259e6aa..78a4b8ec4 100644 --- a/mellea/backends/litellm.py +++ b/mellea/backends/litellm.py @@ -78,16 +78,7 @@ def __init__( base_url: str | None = "http://localhost:11434", model_options: dict | None = None, ): - """Initialize an OpenAI compatible backend using the [LiteLLM Python SDK](https://docs.litellm.ai/docs/#litellm-python-sdk). - - Note: If getting `Unclosed client session`, set `export DISABLE_AIOHTTP_TRANSPORT=True` in your environment. See: https://github.com/BerriAI/litellm/issues/13251. - - Args: - model_id : The LiteLLM model identifier; in most cases requires some combination of `//`. Make sure that all necessary credentials are in OS environment variables. - formatter: A custom formatter based on backend.If None, defaults to TemplateFormatter - base_url : Base url for LLM API. Defaults to None. - model_options : Generation options to pass to the LLM. Defaults to None. - """ + """Initialize a LiteLLM-compatible backend for the given model ID and endpoint.""" super().__init__( model_id=model_id, formatter=( diff --git a/mellea/backends/model_ids.py b/mellea/backends/model_ids.py index 7a85bc408..61e163c9c 100644 --- a/mellea/backends/model_ids.py +++ b/mellea/backends/model_ids.py @@ -27,14 +27,6 @@ class ModelIdentifier: bedrock_name (str | None): AWS Bedrock model ID (e.g. ``"openai.gpt-oss-20b"``). hf_tokenizer_name (str | None): HuggingFace tokenizer ID; defaults to ``hf_model_name`` if ``None``. - Attributes: - hf_model_name (str | None): HuggingFace Hub model repository ID. - ollama_name (str | None): Ollama model tag. - watsonx_name (str | None): WatsonX AI model ID. - mlx_name (str | None): MLX model identifier. - openai_name (str | None): OpenAI API model name. - bedrock_name (str | None): AWS Bedrock model ID. - hf_tokenizer_name (str | None): HuggingFace tokenizer ID; same as ``hf_model_name`` when ``None``. """ hf_model_name: str | None = None diff --git a/mellea/backends/ollama.py b/mellea/backends/ollama.py index a5edd6a0a..014734f2c 100644 --- a/mellea/backends/ollama.py +++ b/mellea/backends/ollama.py @@ -65,16 +65,7 @@ def __init__( base_url: str | None = None, model_options: dict | None = None, ): - """Initializes an ollama backend for local models. - - WARNING: may use up a lot of your machine's memory. - - Args: - model_id (str | ModelIdentifier): Ollama model ID. If ModelIdentifier, then an `ollama_name` must be provided by that ModelIdentifier. - base_url (str): Endpoint that is serving the model API; defaults to env(OLLAMA_HOST) or `http://localhost:11434` - model_options (dict): Ollama model options - formatter (Formatter): formatter for creating input - """ + """Initialize an Ollama backend, connecting to the server and pulling the model if needed.""" super().__init__( model_id=model_id, formatter=( diff --git a/mellea/backends/openai.py b/mellea/backends/openai.py index 9a3a325c4..ad0031189 100644 --- a/mellea/backends/openai.py +++ b/mellea/backends/openai.py @@ -89,8 +89,6 @@ class OpenAIBackend(FormatterBackend): option names to Mellea ``ModelOption`` sentinel keys. from_mellea_model_opts_map_completions (dict): Mapping from Mellea sentinel keys to completions-endpoint option names. - default_to_constraint_checking_alora (bool): Whether aLoRA constraint checking - is enabled by default. """ def __init__( @@ -104,17 +102,7 @@ def __init__( api_key: str | None = None, **kwargs, ): - """Initialize and OpenAI compatible backend. For any additional kwargs that you need to pass the the client, pass them as a part of **kwargs. - - Args: - model_id : A generic model identifier or OpenAI compatible string. Defaults to model_ids.IBM_GRANITE_4_HYBRID_MICRO. - formatter: A custom formatter based on backend.If None, defaults to TemplateFormatter - base_url : Base url for LLM API. Defaults to None. - model_options : Generation options to pass to the LLM. Defaults to None. - default_to_constraint_checking_alora: If set to False then aloras will be deactivated. This is primarily for performance benchmarking and debugging. - api_key : API key for generation. Defaults to None. - kwargs : additional kwargs to pass when creating the OpenAI client. - """ + """Initialize an OpenAI-compatible backend with the given model ID and API credentials.""" super().__init__( model_id=model_id, formatter=( diff --git a/mellea/backends/tools.py b/mellea/backends/tools.py index 609957d7e..125b19938 100644 --- a/mellea/backends/tools.py +++ b/mellea/backends/tools.py @@ -35,9 +35,6 @@ class MelleaTool(AbstractMelleaTool): as_json_tool (dict[str, Any]): The OpenAI-compatible JSON schema dict describing the tool's parameters. - Attributes: - name (str): The registered name of this tool. - as_json_tool (dict[str, Any]): OpenAI-compatible JSON schema dict for this tool. """ # Tool is what we pass as a model option / as input diff --git a/mellea/backends/vllm.py b/mellea/backends/vllm.py index 55e986aa8..bc42af75e 100644 --- a/mellea/backends/vllm.py +++ b/mellea/backends/vllm.py @@ -92,15 +92,7 @@ def __init__( *, model_options: dict | None = None, ): - """Attempt to load model weights using the model_id by default, or using `custom_config` if provided. - - WARNING: initializing a `LocalHFBackend` will download and load the model on your *local* machine. - - Args: - model_id (str | ModelIdentifier): Used to load the model *and tokenizer* via transformers Auto* classes, and then moves the model to the best available device (cuda > mps > cpu). If loading the model and/or tokenizer from a string will not work, or if you want to use a different device string, then you can use custom_config. - formatter (Formatter): A mechanism for turning `stdlib` stuff into strings. Experimental Span-based models should use `mellea.backends.span.*` backends. - model_options (Optional[dict]): Default model options. - """ + """Load model weights via vLLM and initialize the async inference engine.""" formatter = ( formatter if formatter is not None else TemplateFormatter(model_id=model_id) ) diff --git a/mellea/backends/watsonx.py b/mellea/backends/watsonx.py index add1a063e..c356825ee 100644 --- a/mellea/backends/watsonx.py +++ b/mellea/backends/watsonx.py @@ -92,17 +92,7 @@ def __init__( project_id: str | None = None, **kwargs, ): - """A generic watsonx backend that wraps around the ibm_watsonx_ai sdk. - - Args: - model_id : Model id. Defaults to model_ids.IBM_GRANITE_4_HYBRID_SMALL. - formatter : input formatter. Defaults to TemplateFormatter in __init__. - base_url : url for watson ML deployment. Defaults to env(WATSONX_URL). - model_options : Global model options to pass to the model. Defaults to None. - api_key : watsonx API key. Defaults to None. - project_id : watsonx project ID. Defaults to None. - kwargs : extra kwargs passed to model inference creation. - """ + """Initialize a WatsonX AI backend using the ibm_watsonx_ai SDK.""" # There are bugs with the Watsonx python sdk related to async event loops; # using the same watsonx backend across multiple event loops causes errors. warnings.warn( diff --git a/mellea/core/base.py b/mellea/core/base.py index 4cb054ae8..bef046b20 100644 --- a/mellea/core/base.py +++ b/mellea/core/base.py @@ -49,13 +49,7 @@ def __init__( *, cache: bool = False, ): - """Initializes the CBlock with a string and some metadata. - - Args: - value (str | None): the underlying value stored in this CBlock - meta (dict[str, Any] | None): Any meta-information about this CBlock (e.g., the inference engine's Completion object). - cache (bool): If set to `True` then this CBlock's KV cache might be stored by the inference engine. Experimental. - """ + """Initialize CBlock with a string value and optional metadata.""" if value is not None and not isinstance(value, str): raise TypeError("value to a Cblock should always be a string or None") self._underlying_value = value @@ -93,11 +87,7 @@ class ImageBlock(CBlock): """ def __init__(self, value: str, meta: dict[str, Any] | None = None): - """Initializes the ImageBlock with a base64 PNG string representation and some metadata. - - Args: - value (str): A valid base64-encoded PNG string (with or without a data URI prefix). - meta (dict[str, Any] | None): Optional metadata to associate with this image block. + """Initialize ImageBlock with a base64-encoded PNG string, validating the encoding. Raises: AssertionError: If ``value`` is not a valid base64-encoded PNG string. @@ -280,14 +270,7 @@ def __init__( parsed_repr: S | None = None, tool_calls: dict[str, ModelToolCall] | None = None, ): - """Initializes as a CBlock, optionally also with a parsed representation from an output formatter. - - Args: - value (str | None): The raw model output string, or ``None`` if not yet computed. - meta (dict[str, Any] | None): Optional metadata from the inference engine (e.g., completion object). - parsed_repr (S | None): An already-parsed representation to attach; set when re-wrapping existing output. - tool_calls (dict[str, ModelToolCall] | None): Tool calls returned by the model alongside the text output. - """ + """Initialize ModelOutputThunk with an optional pre-computed value and metadata.""" super().__init__(value, meta) self.parsed_repr: S | None = parsed_repr diff --git a/mellea/core/requirement.py b/mellea/core/requirement.py index 71bd276cf..ca780037d 100644 --- a/mellea/core/requirement.py +++ b/mellea/core/requirement.py @@ -26,11 +26,6 @@ class ValidationResult: thunk (ModelOutputThunk | None): The ``ModelOutputThunk`` produced during LLM-as-a-Judge validation, if applicable. context (Context | None): The context associated with the validation backend call, if applicable. - Attributes: - reason (str | None): Human-readable explanation for the pass/fail verdict, if provided. - score (float | None): Optional numeric score returned by the validator. - thunk (ModelOutputThunk | None): The ``ModelOutputThunk`` produced during LLM-as-a-Judge validation, if applicable. - context (Context | None): The context associated with the validation backend call, if applicable. """ def __init__( @@ -42,17 +37,7 @@ def __init__( thunk: ModelOutputThunk | None = None, context: Context | None = None, ): - """The result of a requirement's validation. - - A ValidationResult's result field always contains a definitive pass/fail. The other fields can be used to communicate additional information about that result. - - Args: - result: a boolean that is true if the requirement passed - reason: a reason for the result - score: if your validator gives you a score back, you can add this as metadata - thunk: if your validator utilizes a backend to generate a response, the ModelOutputThunk returned from that request - context: if your validator utilizes a backend to generate a response, the context associated with that response - """ + """Initialize ValidationResult with a pass/fail boolean and optional metadata.""" self._result = result self._reason = reason self._score = score @@ -146,16 +131,7 @@ def __init__( output_to_bool: Callable[[CBlock | str], bool] | None = default_output_to_bool, check_only: bool = False, ): - """A Requirement, interpreted over a Context. - - By default, requirements are validated by the model using LLM-as-a-Judge (or a `constraint` LoRA when available). However, you can also provide a `validate` function with arbitrary behavior. - - Args: - description: A natural-language description of the requirement. This will sometimes be included in `Instruction` prompts; if you do not want the requirement to be included in the prompt to avoid [Purple Elephant Effects](https://${PROJECT_URL}/llm-requirement-engineering-and-purple-elephants/) use check_only=True. - validation_fn: If provided, this function will be executed instead of using LLM-as-a-Judge. The `bool()` for the function's output defines whether the requirement passes. - output_to_bool: An `output_to_bool` may be provided so that the library can translate the LLM-as-a-judge or ALora output into a boolean value. If none is provided, we will look for 'yes' (case-insensitive) in the LLMaJ output. - check_only: If set, then `Instruction` will not include this requirement in its prompt. - """ + """Initialize Requirement with an optional description, validation function, and output converter.""" self.description = description self.output_to_bool = output_to_bool self.validation_fn = validation_fn diff --git a/mellea/core/sampling.py b/mellea/core/sampling.py index 6f3487c1c..0bf75badf 100644 --- a/mellea/core/sampling.py +++ b/mellea/core/sampling.py @@ -31,11 +31,14 @@ class SamplingResult(CBlock, Generic[S]): Attributes: result_index (int): Index into ``sample_generations`` identifying the chosen final output. success (bool): Whether the sampling operation produced a passing result. - sample_generations (list[ModelOutputThunk[S]]): All output thunks generated during sampling. - sample_validations (list[list[tuple[Requirement, ValidationResult]]]): Per-generation validation - results; each inner list contains one tuple per requirement evaluated. - sample_actions (list[Component]): The actions used to produce each generation. - sample_contexts (list[Context]): The contexts associated with each generation. + sample_generations (list[ModelOutputThunk[S]]): All output thunks generated during + sampling; always a list (``None`` input is normalised to ``[]``). + sample_validations (list[list[tuple[Requirement, ValidationResult]]]): Per-generation + validation results; always a list (``None`` input is normalised to ``[]``). + sample_actions (list[Component]): The actions used to produce each generation; + always a list (``None`` input is normalised to ``[]``). + sample_contexts (list[Context]): The contexts associated with each generation; + always a list (``None`` input is normalised to ``[]``). """ def __init__( @@ -49,16 +52,7 @@ def __init__( sample_actions: list[Component] | None = None, sample_contexts: list[Context] | None = None, ): - """Initialize a new instance of sampling results. - - Args: - result_index: The index of the final output or result from applying the sampling strategy. - success: A boolean indicating whether the operation was successful. - sample_generations: A list containing intermediate generations produced during the process. - sample_validations: For each generation a list of tuples of a requirement and a validation result. - sample_actions: A list of intermediate actions used to produce sampling results. - sample_contexts: A list of contexts produced by the generation results. - """ + """Initialize SamplingResult with the chosen output index, success flag, and generation history.""" if sample_generations is None: sample_generations = [] if sample_validations is None: diff --git a/mellea/formatters/granite/intrinsics/input.py b/mellea/formatters/granite/intrinsics/input.py index fe4fbdaf4..c578c62d2 100644 --- a/mellea/formatters/granite/intrinsics/input.py +++ b/mellea/formatters/granite/intrinsics/input.py @@ -238,19 +238,7 @@ def __init__( config_dict: dict | None = None, model_name: str | None = None, ): - """Initialize the IntrinsicsRewriter. - - Args: - config_file (str | pathlib.Path | None): Optional location of the - YAML configuration file. Exactly one of ``config_file`` and - ``config_dict`` must be provided. - config_dict (dict | None): Optional pre-parsed contents of the YAML - configuration file (result of ``yaml.safe_load()``). Exactly one - of ``config_file`` and ``config_dict`` must be provided. - model_name (str | None): Optional model name to inject into chat - completion requests. ``None`` uses the default from the - configuration. Defaults to ``None``. - """ + """Initialize IntrinsicsRewriter from a YAML config file or dict.""" config = make_config_dict(config_file, config_dict) if config is None: raise ValueError("config cannot be None") diff --git a/mellea/formatters/granite/intrinsics/output.py b/mellea/formatters/granite/intrinsics/output.py index fb3ca76b7..c880605dd 100644 --- a/mellea/formatters/granite/intrinsics/output.py +++ b/mellea/formatters/granite/intrinsics/output.py @@ -51,27 +51,13 @@ class TransformationRule(abc.ABC): Attributes: YAML_NAME (str | None): The name used to identify this rule in YAML configuration files. Subclasses must set this to a non-``None`` string. - config (dict): Configuration of the parent output processor, as parsed YAML. - input_path_expr (list[str | int | None]): Path expression matching all - instances of the field that this rule transforms. Elements can be - strings for object fields, integers for list indices, or ``None`` - for wildcard matches. """ YAML_NAME: str | None = None """Subclasses should set this to the name of the rule in YAML config files.""" def __init__(self, config: dict, input_path_expr: list[str | int | None]): - """Initialize the TransformationRule. - - Args: - config (dict): Configuration of the parent output processor, as - parsed YAML. - input_path_expr (list[str | int | None]): Path expression that - matches all instances of the field that this rule transforms. - Elements can be strings for object fields, ints for list - indices, or ``None`` for wildcard matches. - """ + """Initialize TransformationRule with a config dict and a path expression.""" self.config = config self.input_path_expr = input_path_expr @@ -306,9 +292,6 @@ class TokenToFloat(InPlaceTransformation): Attributes: YAML_NAME (str): YAML configuration key for this rule; always ``"likelihood"``. - categories_to_values (dict[str | int | bool, float] | None): Mapping - from categorical label strings or values to their corresponding - floating-point numeric values. """ YAML_NAME = "likelihood" @@ -320,17 +303,7 @@ def __init__( /, categories_to_values: dict[str | int | bool, float] | None = None, ): - """Initialize TokenToFloat transformation rule. - - Args: - config (dict): Configuration of the parent output processor, as - parsed YAML. - input_path_expr (list[str | int | None]): Path expression that - matches all instances of the field that this rule transforms. - categories_to_values (dict[str | int | bool, float] | None): - Mapping from categorical labels to floating-point values. - Defaults to ``None``. - """ + """Initialize TokenToFloat with an optional mapping from categorical labels to float values.""" super().__init__(config, input_path_expr) self.categories_to_values = categories_to_values @@ -521,17 +494,18 @@ class DecodeSentences(AddFieldsTransformation): Attributes: YAML_NAME (str): YAML configuration key for this rule; always ``"decode_sentences"``. - source (str): Where to look for tagged sentences; either - ``"last_message"`` or ``"documents"``. begin_name (str | None): Name of the output field that receives the - sentence begin offset, or ``None`` if not configured. + sentence begin offset; extracted from ``output_names``, or ``None`` + if not configured. end_name (str | None): Name of the output field that receives the - sentence end offset, or ``None`` if not configured. + sentence end offset; extracted from ``output_names``, or ``None`` + if not configured. text_name (str | None): Name of the output field that receives the - sentence text, or ``None`` if not configured. + sentence text; extracted from ``output_names``, or ``None`` if not + configured. document_id_name (str | None): Name of the output field that receives - the document ID (only used when ``source="documents"``), or - ``None`` if not configured. + the document ID (only used when ``source="documents"``); extracted + from ``output_names``, or ``None`` if not configured. """ YAML_NAME = "decode_sentences" @@ -544,18 +518,7 @@ def __init__( source: str, output_names: dict, ): - """Initialize DecodeSentences transformation rule. - - Args: - config (dict): Configuration of the parent output processor, as - parsed YAML. - input_path_expr (list[str | int | None]): Path expression that - matches all instances of the field that this rule transforms. - source (str): Name of the location to look for sentences; must be - ``"last_message"`` or ``"documents"``. - output_names (dict): Mapping from output role name (``"begin"``, - ``"end"``, ``"text"``, ``"document_id"``) to the name of the - new field to add in the result JSON. + """Initialize DecodeSentences with a source location and output field name mapping. Raises: ValueError: If ``source`` is not ``"last_message"`` or @@ -938,14 +901,6 @@ class MergeSpans(InPlaceTransformation): Attributes: YAML_NAME (str): YAML configuration key for this rule; always ``"merge_spans"``. - group_fields (list): Fields used for grouping records before merging - contiguous spans. - begin_field (str): Name of the field that holds the begin offset of - each span. - end_field (str): Name of the field that holds the end offset of each - span. - text_field (str | None): Optional name of the field containing covered - text strings that are concatenated when spans are merged. """ YAML_NAME = "merge_spans" @@ -960,21 +915,7 @@ def __init__( end_field: str, text_field: str | None = None, ): - """Initialize MergeSpans transformation rule. - - Args: - config: Parsed YAML config for IO processing. - input_path_expr: Path expression for the list of records to merge. - group_fields (list): List of fields used for grouping prior to - merging spans. - begin_field (str): Name of the field that holds the begin offset of - spans. - end_field (str): Name of the field that holds the end offset of - spans. - text_field (str | None): Optional field containing covered text - strings that should be concatenated when spans are merged. - Defaults to ``None``. - """ + """Initialize MergeSpans with span field names and optional grouping fields.""" super().__init__(config, input_path_expr) self._type_check("group_fields", group_fields, list) self._type_check("begin_field", begin_field, str) @@ -1242,16 +1183,7 @@ def __init__( config_file: str | pathlib.Path | None = None, config_dict: dict | None = None, ): - """Initialize IntrinsicsResultProcessor. - - Args: - config_file (str | pathlib.Path | None): Optional path to a YAML - configuration file. Exactly one of ``config_file`` and - ``config_dict`` must be provided. - config_dict (dict | None): Optional pre-parsed YAML configuration - dict. Exactly one of ``config_file`` and ``config_dict`` must - be provided. - """ + """Initialize IntrinsicsResultProcessor from a YAML config file or dict.""" config = make_config_dict(config_file, config_dict) if config is None: raise ValueError("config cannot be None") diff --git a/mellea/formatters/granite/retrievers/elasticsearch.py b/mellea/formatters/granite/retrievers/elasticsearch.py index 7db53f0cd..092b52490 100644 --- a/mellea/formatters/granite/retrievers/elasticsearch.py +++ b/mellea/formatters/granite/retrievers/elasticsearch.py @@ -12,11 +12,8 @@ class ElasticsearchRetriever: retrieve the top-k matching documents for a given natural language query. Attributes: - corpus_name (str): Name of the Elasticsearch index to query. hosts (str): Full ``url:port`` connection string to the Elasticsearch - server. - kwargs (dict): Additional keyword arguments forwarded to the - ``Elasticsearch`` client constructor. + server; stored from the ``host`` constructor argument. Args: corpus_name (str): Name of the Elasticsearch index to query. @@ -27,15 +24,7 @@ class ElasticsearchRetriever: """ def __init__(self, corpus_name: str, host: str, **kwargs: Any): - """Initialize ElasticsearchRetriever. - - Args: - corpus_name (str): Name of the Elasticsearch index to query. - host (str): Full ``url:port`` connection string to the Elasticsearch - server. - **kwargs (Any): Additional keyword arguments forwarded to the - ``Elasticsearch`` client constructor. - """ + """Initialize ElasticsearchRetriever with index name and connection details.""" # Third Party from elasticsearch import Elasticsearch # type: ignore[import-not-found] diff --git a/mellea/formatters/granite/retrievers/embeddings.py b/mellea/formatters/granite/retrievers/embeddings.py index 02a595ee7..e1d4c611a 100644 --- a/mellea/formatters/granite/retrievers/embeddings.py +++ b/mellea/formatters/granite/retrievers/embeddings.py @@ -232,16 +232,7 @@ def __init__( data_file_or_table, #: pathlib.Path | str | pa.Table, embedding_model_name: str, ): - """Simple retriever that keeps docs and embeddings in memory. - - Args: - data_file_or_table: Parquet file of document snippets and embeddings, - or an equivalent in-memory PyArrow Table. Should have columns - ``id``, ``begin``, ``end``, ``text``, and ``embedding``. - embedding_model_name: Name of the Sentence Transformers model to use for - embeddings. Must match the model used to compute embeddings in the - data file. - """ + """Initialize InMemoryRetriever by loading embeddings and the sentence transformer model.""" # Third Party import pyarrow as pa import pyarrow.parquet as pq diff --git a/mellea/formatters/template_formatter.py b/mellea/formatters/template_formatter.py index d37786d3d..90c806297 100644 --- a/mellea/formatters/template_formatter.py +++ b/mellea/formatters/template_formatter.py @@ -41,10 +41,6 @@ class TemplateFormatter(ChatFormatter): if you plan to change ``model_id`` or ``template_path`` after construction. Defaults to ``True``. - Attributes: - model_id (str | ModelIdentifier): The model identifier used to locate - model-specific templates during rendering. - Example:: formatter = TemplateFormatter(model_id="my-model", template_path="/path/to/templates") @@ -58,21 +54,7 @@ def __init__( template_path: str = "", use_template_cache: bool = True, ): - """Initialise a ``TemplateFormatter``. - - Args: - model_id (str | ModelIdentifier): Describes the model for which - templates will be looked up. Should match the template - directory structure. - template_path (str): An alternate location where templates can be - found. Will be preferred over all other template directories - even if a less exact match is found. Defaults to ``""``. - use_template_cache (bool): When ``True``, caches the location of - the most recently used templates so that future lookups are - skipped. Set to ``False`` if you plan on changing ``model_id`` - or ``template_path`` after the formatter has been created. - Defaults to ``True``. - """ + """Initialize TemplateFormatter for the given model ID with optional template path and caching.""" self.model_id = model_id self._template_path: str = template_path diff --git a/mellea/helpers/async_helpers.py b/mellea/helpers/async_helpers.py index 6edb90048..0618d54c1 100644 --- a/mellea/helpers/async_helpers.py +++ b/mellea/helpers/async_helpers.py @@ -89,18 +89,12 @@ class ClientCache: capacity (int): Maximum number of entries to hold before evicting the least recently used. Attributes: - capacity (int): Maximum number of entries the cache can hold. - cache (OrderedDict): Ordered dictionary storing the cached key-value pairs in LRU order. + cache (OrderedDict): Ordered dictionary storing cached key-value pairs in LRU + order; always initialised empty at construction. """ def __init__(self, capacity: int): - """Initializes the LRU cache with a certain capacity. - - The `ClientCache` either contains a value or it doesn't. - - Args: - capacity: Maximum number of entries to hold before evicting the least recently used. - """ + """Initialize the client LRU cache with the given capacity.""" self.capacity = capacity self.cache: OrderedDict = OrderedDict() diff --git a/mellea/stdlib/components/chat.py b/mellea/stdlib/components/chat.py index 01b0e1cd9..bea42015d 100644 --- a/mellea/stdlib/components/chat.py +++ b/mellea/stdlib/components/chat.py @@ -41,8 +41,6 @@ class Message(Component["Message"]): Attributes: Role (type): Type alias for the allowed role literals: ``"system"``, ``"user"``, ``"assistant"``, or ``"tool"``. - role (str): The role associated with this message. - content (str): The plain-text content of this message. """ Role = Literal["system", "user", "assistant", "tool"] @@ -55,14 +53,7 @@ def __init__( images: None | list[ImageBlock] = None, documents: None | list[Document] = None, ): - """Initializer for Chat messages. - - Args: - role (str): The role that this message came from (e.g., user, assistant). - content (str): The content of the message. - images (list[ImageBlock]): The images associated with the message if any. - documents (list[Document]): documents associated with the message if any. - """ + """Initialize a Message with a role, text content, and optional images and documents.""" self.role = role self.content = content # TODO this should be private. self._content_cblock = CBlock(self.content) @@ -203,8 +194,8 @@ class ToolMessage(Message): tool (ModelToolCall): The ``ModelToolCall`` representation. Attributes: - name (str): The name of the tool or function that was called. - arguments (Mapping[str, Any]): The arguments that were passed to the tool. + arguments (Mapping[str, Any]): The arguments that were passed to the + tool; stored from the ``args`` constructor parameter. """ def __init__( @@ -216,16 +207,7 @@ def __init__( args: Mapping[str, Any], tool: ModelToolCall, ): - """Initializer for Chat messages. - - Args: - role: the role of this message. Most backends/models use something like tool. - content: The content of the message; should be a stringified version of the tool_output. - name: The name of the tool/function. - args: The args required to call the function. - tool_output: the output of the tool/function call. - tool: the ModelToolCall representation. - """ + """Initialize a ToolMessage with role, content, tool output, name, args, and tool call.""" super().__init__(role, content) self.name = name self.arguments = args diff --git a/mellea/stdlib/components/docs/document.py b/mellea/stdlib/components/docs/document.py index 9aa64452e..6b151d773 100644 --- a/mellea/stdlib/components/docs/document.py +++ b/mellea/stdlib/components/docs/document.py @@ -21,20 +21,10 @@ class Document(Component[str]): title (str | None): An optional human-readable title for the document. doc_id (str | None): An optional unique identifier for the document. - Attributes: - text (str): The text content of this document. - title (str | None): Human-readable title, or ``None`` if not set. - doc_id (str | None): Unique identifier, or ``None`` if not set. """ def __init__(self, text: str, title: str | None = None, doc_id: str | None = None): - """Create a document object. Should typically be used as a list in the ``_docs`` field of ``Message``. - - Args: - text (str): The text content of the document. - title (str | None): An optional human-readable title. - doc_id (str | None): An optional unique identifier. - """ + """Initialize Document with text content and optional title and ID.""" self.text = text self.title = title self.doc_id = doc_id diff --git a/mellea/stdlib/components/docs/richdocument.py b/mellea/stdlib/components/docs/richdocument.py index bb51b18fd..7812112ed 100644 --- a/mellea/stdlib/components/docs/richdocument.py +++ b/mellea/stdlib/components/docs/richdocument.py @@ -36,11 +36,7 @@ class RichDocument(Component[str]): """ def __init__(self, doc: DoclingDocument): - """Create a ``RichDocument`` wrapping the provided ``DoclingDocument``. - - Args: - doc (DoclingDocument): The Docling document to wrap. - """ + """Initialize RichDocument by wrapping the provided DoclingDocument.""" self._doc = doc def parts(self) -> list[Component | CBlock]: @@ -154,12 +150,7 @@ class TableQuery(Query): """ def __init__(self, obj: Table, query: str) -> None: - """Initializes a new instance of the ``TableQuery`` class. - - Args: - obj (Table): The table object to which the query applies. - query (str): The query string. - """ + """Initialize TableQuery for the given table and natural-language query.""" super().__init__(obj, query) def parts(self) -> list[Component | CBlock]: @@ -206,12 +197,7 @@ class TableTransform(Transform): """ def __init__(self, obj: Table, transformation: str) -> None: - """Initializes a new instance of the ``TableTransform`` class. - - Args: - obj (Table): The table object to which the transform applies. - transformation (str): The transformation description string. - """ + """Initialize TableTransform for the given table and transformation description.""" super().__init__(obj, transformation) def parts(self) -> list[Component | CBlock]: @@ -259,13 +245,7 @@ class Table(MObject): """ def __init__(self, ti: TableItem, doc: DoclingDocument): - """Create a ``Table`` wrapping a Docling ``TableItem``. - - Args: - ti (TableItem): The Docling table item to wrap. - doc (DoclingDocument): The parent Docling document. If ``None``, - some Docling extraction functions may fail. - """ + """Initialize Table by wrapping a Docling TableItem and its parent document.""" super().__init__(query_type=TableQuery, transform_type=TableTransform) self._ti = ti self._doc = doc diff --git a/mellea/stdlib/components/genslot.py b/mellea/stdlib/components/genslot.py index 88c22ce32..4ac82486d 100644 --- a/mellea/stdlib/components/genslot.py +++ b/mellea/stdlib/components/genslot.py @@ -107,13 +107,7 @@ def __init__( name: str | None = None, value: str | None = None, ): - """Create an Argument with optional name, annotation, and value. - - Args: - annotation (str | None): The parameter's type annotation as a string. - name (str | None): The parameter name. - value (str | None): The bound value for this parameter as a string. - """ + """Initialize Argument with optional name, type annotation, and bound value.""" self._argument_dict: ArgumentDict = { "name": name, "annotation": annotation, @@ -132,12 +126,7 @@ class Arguments(CBlock): """ def __init__(self, arguments: list[Argument]): - """Create a textual representation of a list of arguments. - - Args: - arguments (list[Argument]): The list of bound function arguments to - render as a formatted string. - """ + """Initialize Arguments by rendering a list of Argument objects as a formatted string.""" # Make meta the original list of arguments and create a list of textual representations. meta: dict[str, Any] = {} text_args = [] @@ -158,17 +147,10 @@ class ArgPreconditionRequirement(Requirement): req (Requirement): The underlying requirement to wrap. All method calls are delegated to this requirement. - Attributes: - req (Requirement): The wrapped underlying requirement instance. """ def __init__(self, req: Requirement): - """Can only be instantiated from existing requirements. All function calls are delegated to the underlying requirement. - - Args: - req (Requirement): The requirement to wrap for argument precondition - validation. - """ + """Initialize ArgPreconditionRequirement by wrapping an existing Requirement.""" self.req = req def __getattr__(self, name): @@ -197,13 +179,7 @@ class PreconditionException(Exception): def __init__( self, message: str, validation_results: list[ValidationResult] ) -> None: - """Exception raised when validation fails for a generative slot's arguments. - - Args: - message (str): The error message describing the precondition failure. - validation_results (list[ValidationResult]): The list of validation - results from the failed preconditions. - """ + """Initialize PreconditionException with a message and the list of failed validation results.""" super().__init__(message) self.validation = validation_results @@ -220,11 +196,7 @@ class Function(Generic[P, R]): """ def __init__(self, func: Callable[P, R]): - """Wrap a callable and capture its metadata via ``describe_function``. - - Args: - func (Callable[P, R]): The callable to wrap. - """ + """Initialize Function by wrapping a callable and capturing its metadata.""" self._func: Callable[P, R] = func self._function_dict: FunctionDict = describe_function(func) @@ -365,10 +337,7 @@ class GenerativeSlot(Component[R], Generic[P, R]): """ def __init__(self, func: Callable[P, R]): - """A generative slot function that converts a given `func` to a generative slot. - - Args: - func: A callable function + """Initialize GenerativeSlot by wrapping the given callable and validating its parameter names. Raises: ValueError: if the decorated function has a parameter name used by generative slots diff --git a/mellea/stdlib/components/instruction.py b/mellea/stdlib/components/instruction.py index 656c1b39f..30faaea20 100644 --- a/mellea/stdlib/components/instruction.py +++ b/mellea/stdlib/components/instruction.py @@ -58,18 +58,7 @@ def __init__( output_prefix: str | CBlock | None = None, images: list[ImageBlock] | None = None, ): - """Initializes an instruction. All strings will be converted into CBlocks. - - Args: - description (str): The description of the instruction. - requirements (List[Requirement | str]): A list of requirements that the instruction can be validated against. - icl_examples (List[str | CBlock]): A list of in-context-learning examples that the instruction can be validated against. - grounding_context (Dict[str, str | CBlock | Component]): A list of grounding contexts that the instruction can use. They can bind as variables using a (key: str, value: str | ContentBlock) tuple. - user_variables (Dict[str, str]): A dict of user-defined variables used to fill in Jinja placeholders in other parameters. This requires that all other provided parameters are provided as strings. - prefix (Optional[str | CBlock]): A prefix string or ContentBlock to use when generating the instruction. - output_prefix (Optional[str | CBlock]): A string or ContentBlock that defines a prefix for the output generation. Usually you do not need this. - images (Optional[List[ImageCBlock]]): A list of images to use in the instruction. - """ + """Initialize Instruction, converting all string inputs to CBlocks and applying any Jinja2 variables.""" requirements = [] if requirements is None else requirements icl_examples = [] if icl_examples is None else icl_examples grounding_context = dict() if grounding_context is None else grounding_context diff --git a/mellea/stdlib/components/intrinsic/intrinsic.py b/mellea/stdlib/components/intrinsic/intrinsic.py index 751d0fb85..497912a91 100644 --- a/mellea/stdlib/components/intrinsic/intrinsic.py +++ b/mellea/stdlib/components/intrinsic/intrinsic.py @@ -35,22 +35,7 @@ class Intrinsic(Component[str]): def __init__( self, intrinsic_name: str, intrinsic_kwargs: dict | None = None ) -> None: - """A component for rewriting messages using intrinsics. - - Intrinsics are special components that transform a chat completion request. - These transformations typically take the form of: - - parameter changes (typically structured outputs) - - adding new messages to the chat - - editing existing messages - - An intrinsic component should correspond to a loaded adapter. - - Args: - intrinsic_name (str): The user-visible name of the intrinsic; must match - a known name in Mellea's intrinsics catalog. - intrinsic_kwargs (dict | None): Some intrinsics require kwargs when - utilizing them; provide them here. - """ + """Initialize Intrinsic by fetching metadata for the named intrinsic from the catalog.""" self.metadata = fetch_intrinsic_metadata(intrinsic_name) if intrinsic_kwargs is None: intrinsic_kwargs = {} diff --git a/mellea/stdlib/components/mobject.py b/mellea/stdlib/components/mobject.py index 0965026c8..38719b213 100644 --- a/mellea/stdlib/components/mobject.py +++ b/mellea/stdlib/components/mobject.py @@ -31,12 +31,7 @@ class Query(Component[str]): """ def __init__(self, obj: Component, query: str) -> None: - """Initializes a new instance of Query with the provided object and query. - - Args: - obj (Component): The object to be queried. - query (str): The query string used for querying the object. - """ + """Initialize Query with the object to query and a natural-language question string.""" self._obj = obj self._query = query @@ -94,12 +89,7 @@ class Transform(Component[str]): """ def __init__(self, obj: Component, transformation: str) -> None: - """Initializes a new instance of Transform with the provided object and transformation description. - - Args: - obj (Component): The object to be transformed. - transformation (str): The string describing the desired transformation. - """ + """Initialize Transform with the object to transform and a natural-language description.""" self._obj = obj self._transformation = transformation @@ -229,13 +219,7 @@ class MObject(Component[str]): def __init__( self, *, query_type: type = Query, transform_type: type = Transform ) -> None: - """Initializes a new instance of MObject with a specified query type and transformation type. - - Args: - query_type (type): The type of query to be used. Defaults to ``Query``. - transform_type (type): The type of transform to be used. Defaults to - ``Transform``. - """ + """Initialize MObject with a query type and transform type for building query/transform components.""" self._query_type = query_type self._transform_type = transform_type diff --git a/mellea/stdlib/components/react.py b/mellea/stdlib/components/react.py index b1b6e63b5..bee9eaf4a 100644 --- a/mellea/stdlib/components/react.py +++ b/mellea/stdlib/components/react.py @@ -46,13 +46,7 @@ class ReactInitiator(Component[str]): """ def __init__(self, goal: str, tools: list[AbstractMelleaTool] | None): - """`ReactInitiator` is used at the start of the ReACT loop to prime the model. - - Args: - goal (str): The objective of the react loop. - tools (list[AbstractMelleaTool] | None): A list of tools that are - available to the react agent; ``None`` is treated as an empty list. - """ + """Initialize ReactInitiator with a goal string and optional list of available tools.""" self.goal = CBlock(goal) self.tools = tools or [] diff --git a/mellea/stdlib/components/unit_test_eval.py b/mellea/stdlib/components/unit_test_eval.py index a22d50947..d2e9a9cf7 100644 --- a/mellea/stdlib/components/unit_test_eval.py +++ b/mellea/stdlib/components/unit_test_eval.py @@ -86,10 +86,6 @@ class TestBasedEval(Component[str]): test_id (str | None): Optional unique identifier for this test. input_ids (list[str] | None): Optional identifiers for each input. - Attributes: - source (str): Origin identifier for this test dataset. - name (str): Human-readable name for this test. - instructions (str): Evaluation guidelines used by the judge model. """ def __init__( @@ -102,18 +98,7 @@ def __init__( test_id: str | None = None, input_ids: list[str] | None = None, ): - """Initialize TestBasedEval (for a single unit test). - - Args: - source (str): Origin identifier for this test dataset. - name (str): Human-readable name for this test. - instructions (str): Evaluation guidelines used by the judge model. - inputs (list[str]): The input texts for each example. - targets (list[list[str]] | None): Expected target strings for each - input. ``None`` is treated as an empty list. - test_id (str | None): Optional unique identifier for this test. - input_ids (list[str] | None): Optional identifiers for each input. - """ + """Initialize TestBasedEval with source, name, instructions, inputs, and optional targets.""" self.source = source self.name = name self.instructions = instructions diff --git a/mellea/stdlib/context.py b/mellea/stdlib/context.py index b1ed669a3..f0e2e8b2f 100644 --- a/mellea/stdlib/context.py +++ b/mellea/stdlib/context.py @@ -23,12 +23,7 @@ class ChatContext(Context): """ def __init__(self, *, window_size: int | None = None): - """Constructs a new chat context. - - Args: - window_size (int | None): Maximum number of context turns to include when - calling ``view_for_generation``. ``None`` means unlimited history. - """ + """Initialize ChatContext with an optional sliding-window size.""" super().__init__() self._window_size = window_size diff --git a/mellea/stdlib/requirements/python_reqs.py b/mellea/stdlib/requirements/python_reqs.py index 623ab863c..ad2d6a74b 100644 --- a/mellea/stdlib/requirements/python_reqs.py +++ b/mellea/stdlib/requirements/python_reqs.py @@ -166,14 +166,7 @@ def __init__( allowed_imports: list[str] | None = None, use_sandbox: bool = False, ): - """Initialize execution validator. - - Args: - timeout: Maximum seconds to allow code to run before timing out. - allow_unsafe_execution: If True, execute code directly with subprocess (unsafe). - allowed_imports: List of allowed import modules when using execution. None means any import is allowed. - use_sandbox: If True, use llm-sandbox for secure Docker-based execution. - """ + """Initialize PythonExecutionReq with execution mode, timeout, and import allowlist settings.""" self._timeout = timeout self._allow_unsafe = allow_unsafe_execution self._allowed_imports = allowed_imports diff --git a/mellea/stdlib/requirements/requirement.py b/mellea/stdlib/requirements/requirement.py index ded1ca248..3ec5e8164 100644 --- a/mellea/stdlib/requirements/requirement.py +++ b/mellea/stdlib/requirements/requirement.py @@ -65,12 +65,7 @@ class ALoraRequirement(Requirement, Intrinsic): """ def __init__(self, description: str, intrinsic_name: str | None = None): - """A requirement that is validated by an ALora. - - Args: - description: See `Requirement.__init__` - intrinsic_name: the name of the intrinsic; must match the adapter - """ + """Initialize ALoraRequirement with a description and optional intrinsic adapter name.""" # TODO: We may want to actually do the validation_fn here so that we can set the score. super().__init__( description, validation_fn=None, output_to_bool=requirement_check_to_bool diff --git a/mellea/stdlib/requirements/safety/guardian.py b/mellea/stdlib/requirements/safety/guardian.py index 141786dc4..26cd632af 100644 --- a/mellea/stdlib/requirements/safety/guardian.py +++ b/mellea/stdlib/requirements/safety/guardian.py @@ -121,20 +121,7 @@ def __init__( tools: list[dict] | None = None, backend: Backend | None = None, ): - """Initialize GuardianCheck using existing backends with minimal glue. - - Args: - risk: The type of risk to check for (harm, jailbreak, etc.) - backend_type: Type of backend to use ("ollama" or "huggingface") - model_version: Specific model version to use - device: Device for model inference (for HuggingFace) - ollama_url: URL for Ollama server - thinking: Enable thinking/reasoning mode - custom_criteria: Custom criteria for validation - context_text: Context document for groundedness checks - tools: Tool schemas for function call validation - backend: Pre-initialized backend to reuse (avoids loading model multiple times) - """ + """Initialize GuardianCheck with a risk type, backend configuration, and optional criteria.""" super().__init__(check_only=True) warnings.warn( diff --git a/mellea/stdlib/sampling/base.py b/mellea/stdlib/sampling/base.py index ea7cd0941..59c044dd0 100644 --- a/mellea/stdlib/sampling/base.py +++ b/mellea/stdlib/sampling/base.py @@ -48,11 +48,6 @@ class BaseSamplingStrategy(SamplingStrategy): requirements (list[Requirement] | None): Global requirements evaluated on every sample. When set, overrides per-call requirements. - Attributes: - loop_budget (int): Maximum number of generate/validate cycles before - falling back to ``select_from_failure``. - requirements (list[Requirement] | None): Global requirements evaluated - on every sample; takes precedence over per-call requirements when set. """ loop_budget: int @@ -60,13 +55,7 @@ class BaseSamplingStrategy(SamplingStrategy): def __init__( self, *, loop_budget: int = 1, requirements: list[Requirement] | None = None ): - """Initialize a new instance of the class with default parameters. - - Args: - loop_budget: Number of times to iterate through the process. Must be greater than 0. - validate: Function to validate the results against requirements. If None, validation is provided later through setter. - generate: Function to generate new model output thunks. If None, generate is provided later through setter. - requirements: List of requirements to test against. If None, test all requirements attached to the given instruction. + """Initialize BaseSamplingStrategy with a loop budget and optional global requirements. Raises: AssertionError: If loop_budget is not greater than 0. diff --git a/mellea/stdlib/sampling/budget_forcing.py b/mellea/stdlib/sampling/budget_forcing.py index 3fb10247a..d17402e57 100644 --- a/mellea/stdlib/sampling/budget_forcing.py +++ b/mellea/stdlib/sampling/budget_forcing.py @@ -51,21 +51,6 @@ class BudgetForcingSamplingStrategy(RejectionSamplingStrategy): loop_budget (int): Rejection-sampling loop count. Must be > 0. requirements (list[Requirement] | None): Requirements to validate against. - Attributes: - think_max_tokens (int | None): Maximum tokens allocated for the thinking - block. ``None`` means no budget (zero-think case). - answer_max_tokens (int | None): Maximum tokens allocated for the answer - block. ``None`` means unbounded (generate until EoS). - start_think_token (str | None): Token that opens the thinking block, - e.g. ``""``. - end_think_token (str | None): Token that closes the thinking block, - e.g. ``""``. - begin_response_token (str | None): Optional token that opens the - response block, e.g. ``""``. - end_response_token (str): Token that closes the response block. - think_more_suffix (str | None): Suffix appended to force continued - thinking. Empty string disables forced thinking. - answer_suffix (str | None): Suffix appended to elicit a final answer. """ think_max_tokens: int | None @@ -91,21 +76,7 @@ def __init__( loop_budget: int = 1, requirements: list[Requirement] | None, ): - r"""Initialize class. - - Inherits from RejectionSamplingStrategy. - - Args: - think_max_tokens: Number of tokens for think block - answer_max_tokens: Number of tokens allocated for answer portion, if set to None answer tokens will be unlimited - start_think_token: Special start of think block token defaults to '' - end_think_token: Special end of think block token defaults to '' - begin_response_token: Special begin of response block token e.g. '' defaults to "" - end_response_token: Special end of response block token e.g. '' defaults to "" - think_more_suffix: Suffix for continue thinking e.g. "\nWait let's think more carefully" to force the model to think more, defaults to "". If set to "", no force thinking will be applied, the token budget will be become an upper bound. - answer_suffix: Suffix to obtain final answer, default to "\nThe final answer is:" - loop_budget: Number of times to iterate through the process. Must be greater than 0. - requirements: List of requirements to test against. If None, test all requirements attached to the given instruction. + r"""Initialize BudgetForcingSamplingStrategy with token budget parameters and rejection-sampling settings. Raises: AssertionError: If loop_budget is not greater than 0. diff --git a/mellea/stdlib/sampling/majority_voting.py b/mellea/stdlib/sampling/majority_voting.py index 48e50e198..05b377c32 100644 --- a/mellea/stdlib/sampling/majority_voting.py +++ b/mellea/stdlib/sampling/majority_voting.py @@ -32,11 +32,9 @@ class BaseMBRDSampling(RejectionSamplingStrategy): against. If ``None``, uses per-call requirements. Attributes: - number_of_samples (int): Number of candidate samples to draw and compare. - weighted (bool): Whether to apply per-sample weights before voting. - Currently not implemented; reserved for future use. symmetric (bool): Whether the similarity metric is symmetric, allowing - the upper-triangle score matrix to be mirrored. + the upper-triangle score matrix to be mirrored; always ``True`` for + this base class. """ number_of_samples: int @@ -51,18 +49,7 @@ def __init__( loop_budget: int = 1, requirements: list[Requirement] | None = None, ): - """Initialize a new abstract Minimum Bayes Risk Decoding (MBRD) Sampling Strategy with default parameters. - - Inherits from RejectionSamplingStrategy. Will generate up to loop_budget x number_of_samples requests. If no - requirements are provided here or in sample(...), will only generate number_of_samples requests. - - Classes that inherit from this must implement the `compare_strings` function. - - Args: - number_of_samples: Number of samples to generate and use for majority voting - weighted: Not Implemented. If True, weights the score before getting the final majority vote - loop_budget: Inner rejection sampling number of times to iterate through the process. Must be greater than 0. - requirements: List of requirements to test against. If None, test all requirements attached to the given instruction. + """Initialize BaseMBRDSampling with the number of samples, weighting flag, and inner loop budget. Raises: AssertionError: If loop_budget is not greater than 0. @@ -214,15 +201,10 @@ class MajorityVotingStrategyForMath(BaseMBRDSampling): requirements (list[Requirement] | None): Requirements to validate against. Attributes: - number_of_samples (int): Number of candidate samples to generate. match_types (list[str]): Extraction target types used for parsing math - expressions (e.g. ``["latex", "axpr"]``). - float_rounding (int): Number of decimal places used when comparing - floating-point results. - strict (bool): Whether strict comparison mode is enforced (variables - matter; sets are not comparable with tuples). - allow_set_relation_comp (bool): Whether set-relation comparisons are - permitted in all cases. + expressions; always ``["latex", "axpr"]``, computed at init. + symmetric (bool): Inherited from ``BaseMBRDSampling``; always ``True`` + for this strategy (set explicitly at init). """ number_of_samples: int @@ -242,23 +224,7 @@ def __init__( loop_budget: int = 1, requirements: list[Requirement] | None = None, ): - """Initialize a new instance of MajorityVoting Sampling Strategy for Math with default parameters. - - Will generate up to loop_budget x number_of_samples requests. If no - requirements are provided here or in sample(...), will only generate number_of_samples requests. - - Args: - number_of_samples: Number of samples to generate and use for majority voting - float_rounding: Number of decimal places to round floats to. Defaults to 6. - strict: Whether to enforce strict comparison mode. Defaults to True. - - In strict mode: Variables matter and sets are not comparable with tuples - - In non-strict mode: Variables are matched by position and sets can be compared with tuples - allow_set_relation_comp: Whether to allow set - relation (e.g 1 < x < 2 and (1, 2)) comparison. Defaults to False. - - If True, set - relation comparison will be allowed in all cases. - - If False, set - relation comparison will be allowed only if the prediction is a set. - weighted: Not Implemented. If True, weights the score before getting the final majority vote - loop_budget: Inner rejection sampling number of times to iterate through the process. Must be greater than 0. - requirements: List of requirements to test against. If None, test all requirements attached to the given instruction. + """Initialize MajorityVotingStrategyForMath with math-comparison settings and sampling parameters. Raises: AssertionError: If loop_budget is not greater than 0. @@ -349,16 +315,7 @@ def __init__( loop_budget: int = 1, requirements: list[Requirement] | None = None, ): - """Initialize a new instance of MBRDRougeL Sampling Strategy with default parameters. - - Will generate up to loop_budget x number_of_samples requests. If no - requirements are provided here or in sample(...), will only generate number_of_samples requests. - - Args: - number_of_samples: Number of samples to generate and use for majority voting - weighted: Not Implemented. If True, weights the score before getting the final majority vote - loop_budget: Inner rejection sampling number of times to iterate through the process. Must be greater than 0. - requirements: List of requirements to test against. If None, test all requirements attached to the given instruction. + """Initialize MBRDRougeLStrategy with RougeL scoring and sampling parameters. Raises: AssertionError: If loop_budget is not greater than 0. diff --git a/mellea/stdlib/sampling/sofai.py b/mellea/stdlib/sampling/sofai.py index c08277f55..0f17e8c1f 100644 --- a/mellea/stdlib/sampling/sofai.py +++ b/mellea/stdlib/sampling/sofai.py @@ -57,16 +57,6 @@ class SOFAISamplingStrategy(SamplingStrategy): feedback_strategy (Literal["simple", "first_error", "all_errors"]): Detail level of repair feedback provided to the S1 solver. - Attributes: - s1_solver_backend (Backend): Backend for the fast S1 solver. - s2_solver_backend (Backend): Backend for the slow S2 solver. - s2_solver_mode (str): How to invoke the S2 solver: ``"fresh_start"``, - ``"continue_chat"``, or ``"best_attempt"``. - loop_budget (int): Maximum S1 attempts before escalating to S2. - judge_backend (Backend | None): Optional third backend for LLM-as-Judge - validation. ``None`` uses the session backend. - feedback_strategy (str): Detail level of feedback: ``"simple"``, - ``"first_error"``, or ``"all_errors"``. """ def __init__( @@ -81,24 +71,7 @@ def __init__( judge_backend: Backend | None = None, feedback_strategy: Literal["simple", "first_error", "all_errors"] = "simple", ): - """Initialize SOFAI sampling strategy with two solvers. - - Args: - s1_solver_backend: Backend for S1 Solver (fast model for iterative solving). - s2_solver_backend: Backend for S2 Solver (slow model for escalation). - s2_solver_mode: How to invoke S2 Solver: - - "fresh_start": Same prompt as S1 solver (clean slate) - - "continue_chat": Fresh start input + S1 iteration/feedback history - - "best_attempt": Fresh start input + best S1 attempt + its feedback - loop_budget: Maximum attempts for S1 Solver before falling back to S2 Solver. - judge_backend: Optional third backend for LLM-as-Judge validation. - If provided, this backend will be used for validation when no custom - validation_fn is provided. Priority: validation_fn > judge_backend > session backend. - feedback_strategy: Control detail level of LLM-as-Judge feedback: - - "simple": Binary yes/no validation, no detailed feedback (default) - - "first_error": Provide only the first mistake found with detailed feedback - - "all_errors": Provide comprehensive feedback about all mistakes - Note: Only used when judge_backend is provided and requirement has no validation_fn. + """Initialize SOFAISamplingStrategy with S1/S2 solver backends, loop budget, and feedback settings. Raises: TypeError: If backends are not Backend instances. diff --git a/mellea/stdlib/session.py b/mellea/stdlib/session.py index 85e4ae83d..acdd42c67 100644 --- a/mellea/stdlib/session.py +++ b/mellea/stdlib/session.py @@ -291,20 +291,16 @@ class MelleaSession: ``SimpleContext`` if ``None``. Attributes: - ctx (Context): The active conversation context. Updated after every call that - produces model output. - backend (Backend): The backend used for model inference throughout the session. + ctx (Context): The active conversation context; never ``None`` (defaults + to a fresh ``SimpleContext`` when ``None`` is passed). Updated after + every call that produces model output. + id (str): Unique session UUID assigned at construction. """ ctx: Context def __init__(self, backend: Backend, ctx: Context | None = None): - """Initializes a new Mellea session with the provided backend and context. - - Args: - backend (Backend): This is always required. - ctx (Context): The way in which the model's context will be managed. By default, each interaction with the model is a stand-alone interaction, so we use SimpleContext as the default. - """ + """Initialize MelleaSession with a backend and optional conversation context.""" import uuid self.id = str(uuid.uuid4()) diff --git a/mellea/stdlib/tools/interpreter.py b/mellea/stdlib/tools/interpreter.py index 5554b21ed..237545621 100644 --- a/mellea/stdlib/tools/interpreter.py +++ b/mellea/stdlib/tools/interpreter.py @@ -51,19 +51,6 @@ class ExecutionResult: analysis_result (Any | None): Optional payload from static-analysis environments. - Attributes: - success (bool): ``True`` if the code executed with exit code 0 or if a - static-analysis check passed; ``False`` otherwise. - stdout (str | None): Standard output captured from the subprocess, or - ``None`` if execution was skipped. - stderr (str | None): Standard error captured from the subprocess, or - ``None`` if execution was skipped. - skipped (bool): ``True`` when execution was not attempted (e.g. due to - unauthorized imports or a missing sandbox dependency). - skip_message (str | None): Human-readable explanation of why execution - was skipped, when ``skipped`` is ``True``. - analysis_result (Any | None): Optional payload returned by static-analysis - environments (e.g. the parsed AST tree or a ``SyntaxError`` instance). """ success: bool @@ -110,18 +97,10 @@ class ExecutionEnvironment(ABC): allowed_imports (list[str] | None): Allowlist of top-level module names that generated code may import. ``None`` disables the import check. - Attributes: - allowed_imports (list[str] | None): Allowlist of top-level module names that - generated code may import. ``None`` disables the import check entirely. """ def __init__(self, allowed_imports: list[str] | None = None): - """Initialize with optional import restrictions. - - Args: - allowed_imports (list[str] | None): List of allowed import modules. - ``None`` means any import is allowed. - """ + """Initialize ExecutionEnvironment with an optional import allowlist.""" self.allowed_imports = allowed_imports @abstractmethod diff --git a/tooling/docs-autogen/audit_coverage.py b/tooling/docs-autogen/audit_coverage.py index 772256e4d..caf93c831 100755 --- a/tooling/docs-autogen/audit_coverage.py +++ b/tooling/docs-autogen/audit_coverage.py @@ -238,21 +238,23 @@ def _check_member(member, full_path: str, short_threshold: int) -> list[dict]: } ) - # Attributes section check: flag when public non-method attributes exist - pub_attrs = [ - n - for n, m in member.members.items() - if not n.startswith("_") and getattr(m, "is_attribute", False) - ] - if pub_attrs and not _ATTRIBUTES_RE.search(doc_text): - sample = ", ".join(pub_attrs[:3]) + ("..." if len(pub_attrs) > 3 else "") - issues.append( - { - "path": full_path, - "kind": "no_attributes", - "detail": f"public attributes [{sample}] have no Attributes section", - } + # Duplicate Args check: Option C requires Args: on the class docstring only. + # Flag when __init__ has its own Args: section in addition to the class's. + init_doc = getattr(init, "docstring", None) + init_doc_text = ( + init_doc.value.strip() if (init_doc and init_doc.value) else "" ) + if _ARGS_RE.search(init_doc_text) and _ARGS_RE.search(doc_text): + issues.append( + { + "path": full_path, + "kind": "duplicate_init_args", + "detail": ( + "Args: in both class and __init__ docstrings " + "(Option C: place Args: on class docstring only)" + ), + } + ) return issues @@ -272,10 +274,15 @@ def audit_docstring_quality( - no_args: function with parameters but no Args/Parameters section - no_returns: function with a non-trivial return annotation but no Returns section - no_raises: function whose source contains raise but has no Raises section - - no_class_args: class whose __init__ has typed params but no Args section - - no_attributes: class with public attributes but no Attributes section + - no_class_args: class whose __init__ has typed params but no Args section on the class + - duplicate_init_args: Args: present in both class docstring and __init__ (Option C violation) - param_mismatch: Args section documents names absent from the real signature + Note: Attributes: sections are intentionally not enforced. Under the Option C + convention, Attributes: is only used when stored values differ in type or + behaviour from the constructor inputs (e.g. type transforms, computed values, + class constants). Pure-echo entries that repeat Args: verbatim are omitted. + Only symbols (and methods whose parent class) present in `documented` are checked when that set is provided — ensuring the audit is scoped to what is actually surfaced in the API reference. @@ -372,7 +379,7 @@ def _print_quality_report(issues: list[dict]) -> None: "no_returns": "Missing Returns section", "no_raises": "Missing Raises section", "no_class_args": "Missing class Args section", - "no_attributes": "Missing Attributes section", + "duplicate_init_args": "Duplicate Args: in class + __init__ (Option C violation)", "param_mismatch": "Param name mismatches (documented but not in signature)", } @@ -389,7 +396,7 @@ def _print_quality_report(issues: list[dict]) -> None: "no_returns", "no_raises", "no_class_args", - "no_attributes", + "duplicate_init_args", "param_mismatch", ): items = by_kind.get(kind, []) From 818007e63121afad5ccfdf4f4d7dafea84fd6450 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 12:36:16 +0000 Subject: [PATCH 10/14] docs: remove isolated venv from generate-ast.py, fix audit_coverage.py (#613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - generate-ast.py: remove isolated .venv-docs-autogen — use the project venv (sys.executable) directly; mdxify and mellea are already installed via uv sync --all-extras --group dev; keep clean MDXIFY_CWD for import safety; remove --no-venv / --pypi-name / --pypi-version args and ensure_venv / pip_install helpers; update module docstring - audit_coverage.py: fix no_class_args to filter variadic *args/**kwargs by ParameterKind (matching the existing function check) so SimpleComponent (**kwargs) is not falsely flagged; update find_documented_symbols to match mdxify 0.2.37 heading format (### `SymbolName`) - mellea/core/utils.py: add Args: to RESTHandler class docstring (missed in Option C sweep); remove pure-echo Attributes: section --- mellea/core/utils.py | 7 +- tooling/docs-autogen/audit_coverage.py | 37 +++---- tooling/docs-autogen/generate-ast.py | 140 ++++++------------------- 3 files changed, 55 insertions(+), 129 deletions(-) diff --git a/mellea/core/utils.py b/mellea/core/utils.py index 66ac33e8a..91d87ee39 100644 --- a/mellea/core/utils.py +++ b/mellea/core/utils.py @@ -21,10 +21,11 @@ class RESTHandler(logging.Handler): variable is set. Failures are silently suppressed to avoid disrupting the application. - Attributes: + Args: api_url (str): The URL of the REST endpoint that receives log records. - method (str): The HTTP method used when sending records (default ``"POST"``). - headers (dict): HTTP headers sent with each request; defaults to ``{"Content-Type": "application/json"}``. + method (str): HTTP method to use when sending records (default ``"POST"``). + headers (dict | None): HTTP headers to send; defaults to + ``{"Content-Type": "application/json"}`` when ``None``. """ def __init__( diff --git a/tooling/docs-autogen/audit_coverage.py b/tooling/docs-autogen/audit_coverage.py index caf93c831..28e57fd72 100755 --- a/tooling/docs-autogen/audit_coverage.py +++ b/tooling/docs-autogen/audit_coverage.py @@ -215,7 +215,12 @@ def _check_member(member, full_path: str, short_threshold: int) -> list[dict]: ) if getattr(member, "is_class", False): - # Args section check for classes: look at __init__ typed parameters + # Args section check for classes: look at __init__ typed parameters. + # Variadic *args/**kwargs are excluded by kind (same logic as function check). + _variadic_kinds = { + griffe.ParameterKind.var_positional, + griffe.ParameterKind.var_keyword, + } init = member.members.get("__init__") if init: init_params = getattr(init, "parameters", None) @@ -223,7 +228,7 @@ def _check_member(member, full_path: str, short_threshold: int) -> list[dict]: p.name for p in (init_params or []) if p.name not in ("self", "cls") - and not p.name.startswith("*") + and getattr(p, "kind", None) not in _variadic_kinds and p.annotation is not None ] if typed_params and not _ARGS_RE.search(doc_text): @@ -505,23 +510,19 @@ def find_documented_symbols(docs_dir: Path) -> set[str]: # Read file to find documented symbols content = mdx_file.read_text() - # Look for heading patterns that indicate symbol documentation - # The actual format is: ### ...FUNC `symbol_name` - # or: ### ...CLASS `ClassName` - - # Match both old format and new format - # Old: ## class Base, ## function generative - # New: ### ...FUNC `blockify`, ### ...CLASS `Component` - old_pattern = r"^##\s+(?:class|function|attribute)\s+(\w+)" - new_pattern = r"###\s+]*>(?:FUNC|CLASS|ATTR)\s+`(\w+)`" - - for match in re.finditer(old_pattern, content, re.MULTILINE): - symbol_name = match.group(1) - documented.add(f"{module_path}.{symbol_name}") + # Look for heading patterns that indicate symbol documentation. + # mdxify output format (0.2.37+): ### `SymbolName` ... + # Legacy formats kept for compatibility with older generated files. + patterns = [ + r"^##\s+(?:class|function|attribute)\s+(\w+)", # very old + r"###\s+]*>(?:FUNC|CLASS|ATTR)\s+`(\w+)`", # intermediate + r"^###\s+`(\w+)`", # current mdxify 0.2.37+ + ] - for match in re.finditer(new_pattern, content, re.MULTILINE): - symbol_name = match.group(1) - documented.add(f"{module_path}.{symbol_name}") + for pattern in patterns: + for match in re.finditer(pattern, content, re.MULTILINE): + symbol_name = match.group(1) + documented.add(f"{module_path}.{symbol_name}") return documented diff --git a/tooling/docs-autogen/generate-ast.py b/tooling/docs-autogen/generate-ast.py index aeb8976d5..5687eee04 100644 --- a/tooling/docs-autogen/generate-ast.py +++ b/tooling/docs-autogen/generate-ast.py @@ -1,31 +1,22 @@ #!/usr/bin/env python3 -"""generate-ast.py (INSTALL mode, then mdxify + postprocess) +"""generate-ast.py — mdxify + postprocess docs pipeline. -Key fix vs your prior script: - - mdxify is executed with cwd set to a clean temp directory OUTSIDE the repo root, - so Python imports the INSTALLED distribution (site-packages) and does not get - shadowed by repo-local ./mellea or ./cli packages. +Runs mdxify against the project's mellea and cli packages then postprocesses +the generated MDX files into the Mintlify docs tree. + +Requires mdxify to be installed in the current Python environment. Run via:: + + uv run python tooling/docs-autogen/generate-ast.py Pipeline: - 1) Use current Python environment (assumes mdxify is installed) - 2) Run mdxify --all for root modules: mellea, cli into STAGING: /docs/api/ - 4) Reorganize flat mdxify output into nested folders - 5) Rename __init__.mdx -> .mdx (dedupe if identical) - 6) Update frontmatter (title/sidebarTitle/description) from H1 + first paragraph - 7) Remove truly-empty MDX files - 8) Move generated docs to /api (replace existing) - 9) Build Mintlify API Reference nav (NO .mdx suffix) - 10) Merge by replacing ONLY: { "tab": "API Reference", ... } in docs.json - -Usage: - python3 tooling/docs-autogen/generate-ast.py \ - --docs-json docs/docs/docs.json \ - --docs-root docs/docs \ - --pypi-name mellea \ - --pypi-version 0.3.0 - -Notes: - - --pypi-version may be "v0.3.0" or "0.3.0". If omitted, installs latest. + 1) Run mdxify --all for root modules: mellea, cli into STAGING: /docs/api/ + 2) Reorganize flat mdxify output into nested folders + 3) Rename __init__.mdx -> .mdx (dedupe if identical) + 4) Update frontmatter (title/sidebarTitle/description) from H1 + first paragraph + 5) Remove truly-empty MDX files + 6) Move generated docs to /api (replace existing) + 7) Build Mintlify API Reference nav (NO .mdx suffix) + 8) Merge by replacing ONLY: { "tab": "API Reference", ... } in docs.json """ import argparse @@ -49,9 +40,9 @@ STAGING_DOCS_ROOT = REPO_ROOT / "docs" STAGING_API_DIR = STAGING_DOCS_ROOT / "api" -# Venv + clean run dir (outside import shadowing) -VENV_DIR = REPO_ROOT / ".venv-docs-autogen" -MDXIFY_CWD = REPO_ROOT / ".mdxify-run-cwd" # must NOT contain mellea/ or cli/ packages +# Clean run dir for mdxify (outside the repo root so repo-local packages +# cannot shadow site-packages via directory-level import precedence) +MDXIFY_CWD = REPO_ROOT / ".mdxify-run-cwd" # If you want explicit link backing, keep repo-url. If you want mdxify auto-detection, omit. REPO_URL = "https://github.com/generative-computing/mellea" @@ -60,14 +51,6 @@ # ----------------------------- # Helpers # ----------------------------- -def normalize_version(v: str | None) -> str | None: - if not v: - return None - v = v[1:] if v.startswith("v") else v - # Return None for branch names or non-version strings (must start with digit) - if not v or not v[0].isdigit(): - return None - return v def yaml_quote(value: str | None) -> str: @@ -158,52 +141,15 @@ def merge_api_reference_into_docs_json( print(f"✅ Merged API Reference tab into: {docs_json_path}", flush=True) -# ----------------------------- -# Venv + installs -# ----------------------------- -def ensure_venv() -> Path: - if not VENV_DIR.exists(): - print(f"🧪 Creating venv: {VENV_DIR}", flush=True) - subprocess.run([sys.executable, "-m", "venv", str(VENV_DIR)], check=True) - - py = VENV_DIR / ("Scripts" if os.name == "nt" else "bin") / "python" - if not py.exists(): - raise RuntimeError(f"Venv python not found at: {py}") - return py - - -def pip_install(venv_python: Path, pypi_name: str, pypi_version: str | None) -> None: - ver = normalize_version(pypi_version) - # If no version specified, install from local source (for development) - # Otherwise install from PyPI with specified version - if not ver: - spec = "." # Install from current directory - print( - f"📦 Installing into venv: mdxify + {pypi_name} (from local source)", - flush=True, - ) - else: - spec = f"{pypi_name}=={ver}" - print(f"📦 Installing into venv: mdxify + {spec}", flush=True) - - subprocess.run([str(venv_python), "-m", "pip", "install", "-U", "pip"], check=True) - subprocess.run( - [str(venv_python), "-m", "pip", "install", "-U", "mdxify", spec], - check=True, - cwd=str(REPO_ROOT) if not ver else None, - ) - - # ----------------------------- # mdxify generation # ----------------------------- -def run_mdxify_generation( - venv_python: Path, root_module: str, source_root: Path | None = None -) -> None: +def run_mdxify_generation(root_module: str, source_root: Path | None = None) -> None: output_dir = STAGING_API_DIR / root_module output_dir.mkdir(parents=True, exist_ok=True) - # Critical: run mdxify from a clean cwd so repo-local packages don't shadow site-packages + # Run mdxify from a clean cwd so repo-local source directories don't take + # precedence over site-packages via directory-level import shadowing. if MDXIFY_CWD.exists(): shutil.rmtree(MDXIFY_CWD) MDXIFY_CWD.mkdir(parents=True, exist_ok=True) @@ -214,7 +160,7 @@ def run_mdxify_generation( ) cmd = [ - str(venv_python), + sys.executable, "-m", "mdxify", "--all", @@ -230,12 +176,14 @@ def run_mdxify_generation( env = os.environ.copy() if source_root is not None: - # Point mdxify at the external source tree so it imports the right package. - # We set PYTHONPATH rather than clearing it so the venv site-packages - # (mdxify itself) remain importable. - env["PYTHONPATH"] = str(source_root) + # Point mdxify at an external source tree (--source-dir). + # Prepend it to PYTHONPATH so it takes precedence over any installed copy. + existing = env.get("PYTHONPATH", "") + env["PYTHONPATH"] = ( + f"{source_root}:{existing}" if existing else str(source_root) + ) else: - # Default: clear PYTHONPATH so the repo-local packages don't shadow site-packages + # Default: drop PYTHONPATH so no repo-local directory shadows site-packages. env.pop("PYTHONPATH", None) subprocess.run(cmd, check=True, text=True, cwd=str(MDXIFY_CWD), env=env) @@ -751,7 +699,7 @@ def build_and_merge_navigation( # ----------------------------- def main() -> None: parser = argparse.ArgumentParser( - description="Install mellea + mdxify in a venv, generate MDX API docs, postprocess, move to docs root, merge nav." + description="Generate MDX API docs with mdxify, postprocess, and merge into the Mintlify docs tree." ) parser.add_argument( "--docs-json", required=False, help="Path to docs.json to update." @@ -761,21 +709,6 @@ def main() -> None: required=False, help="Mintlify docs root (defaults to parent of docs.json).", ) - parser.add_argument( - "--pypi-name", - default="mellea", - help="PyPI project name to install (default: mellea).", - ) - parser.add_argument( - "--pypi-version", - required=False, - help="Version like v0.3.0 or 0.3.0. Omit for latest.", - ) - parser.add_argument( - "--no-venv", - action="store_true", - help="Skip virtual environment creation (use when called via 'uv run --with').", - ) parser.add_argument( "--nav-only", action="store_true", @@ -828,18 +761,9 @@ def main() -> None: shutil.rmtree(STAGING_API_DIR) STAGING_API_DIR.mkdir(parents=True, exist_ok=True) - if args.no_venv: - print("⚠️ Skipping venv creation (--no-venv flag set)", flush=True) - venv_python = Path(sys.executable) - else: - venv_python = ensure_venv() - pip_install(venv_python, args.pypi_name, args.pypi_version) - - # Generate MDX into staging (critical cwd fix inside run_mdxify_generation) + # Generate MDX into staging (clean cwd inside run_mdxify_generation) for pkg in PACKAGES: - run_mdxify_generation( - venv_python, pkg, source_root if source_root != REPO_ROOT else None - ) + run_mdxify_generation(pkg, source_root if source_root != REPO_ROOT else None) # Restructure + cleanup in staging reorganize_to_nested_structure() From 770be344c8e83c66adc9d9f555883b4836ef396b Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 12:42:42 +0000 Subject: [PATCH 11/14] docs: add docstring validation guidance to CONTRIBUTING.md (#613) Add a 'Validating docstrings' subsection under the class docstring placement rule with: - audit_coverage.py command to run after generate-ast.py - Table of key quality checks (no_class_args, duplicate_init_args, etc.) - Three real classes to hover over in VS Code to verify IDE UX --- CONTRIBUTING.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea9019b8c..cbd416a2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -174,6 +174,35 @@ differs in type or behaviour from the constructor input — for example, when a argument is wrapped into a `CBlock`, or when a class-level constant is relevant to callers. Pure-echo entries that repeat `Args:` verbatim should be omitted. +#### Validating docstrings + +Run the coverage and quality audit to check your changes before committing: + +```bash +# Build fresh API docs then audit quality (documented symbols only) +uv run python tooling/docs-autogen/generate-ast.py +uv run python tooling/docs-autogen/audit_coverage.py \ + --quality --no-methods --docs-dir docs/docs/api +``` + +Key checks the audit enforces: + +| Check | Meaning | +|-------|---------| +| `no_class_args` | Class has typed `__init__` params but no `Args:` on the class docstring | +| `duplicate_init_args` | `Args:` appears in both the class and `__init__` docstrings (Option C violation) | +| `no_args` | Standalone function has params but no `Args:` section | +| `no_returns` | Function has a non-trivial return annotation but no `Returns:` section | +| `param_mismatch` | `Args:` documents names not present in the actual signature | + +**IDE hover verification** — open any of these existing classes in VS Code and hover +over the class name or a constructor call to confirm the hover card shows `Args:` once +with no duplication: + +- `ReactInitiator` ([mellea/stdlib/components/react.py](mellea/stdlib/components/react.py)) — `Args:` + `Attributes:` (`goal: str → CBlock` transform) +- `BaseSamplingStrategy` ([mellea/stdlib/sampling/base.py](mellea/stdlib/sampling/base.py)) — `Args:` only, no `Attributes:` (pure-echo removed) +- `TokenToFloat` ([mellea/formatters/granite/intrinsics/output.py](mellea/formatters/granite/intrinsics/output.py)) — `Attributes:` for `YAML_NAME` class constant + ### Code Style - **Ruff** for linting and formatting From b1e826d0365ce6a8f1d422a6bc0f79609c83f9b9 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 13:04:05 +0000 Subject: [PATCH 12/14] docs: add class/__init__ docstring placement rule to docs guide (#613) The docs contribution guide only showed the function docstring pattern. Add the class docstring placement rule (Option C): Args: on class only, __init__ gets a summary sentence, Attributes: only for genuine transforms. Link back to CONTRIBUTING.md for the full validation workflow. --- docs/docs/guide/CONTRIBUTING.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/docs/guide/CONTRIBUTING.md b/docs/docs/guide/CONTRIBUTING.md index 99473f92f..3274cef65 100644 --- a/docs/docs/guide/CONTRIBUTING.md +++ b/docs/docs/guide/CONTRIBUTING.md @@ -305,6 +305,8 @@ No version tags on individual features yet — incomplete tagging misleads reade Mellea uses **Google-style docstrings**. These feed the auto-generated API reference. +**Functions** — `Args:` and `Returns:` on the function docstring: + ```python def my_function(arg: str) -> bool: """One-line summary. @@ -320,6 +322,30 @@ def my_function(arg: str) -> bool: """ ``` +**Classes** — `Args:` on the *class* docstring only; `__init__` gets a single summary sentence. +The docs pipeline skips `__init__`, so `Args:` must live on the class to appear in the API reference: + +```python +class MyComponent(Component[str]): + """A component that does something useful. + + Args: + name (str): Human-readable label for this component. + max_tokens (int): Upper bound on generated tokens. + """ + + def __init__(self, name: str, max_tokens: int = 256) -> None: + """Initialize MyComponent with a name and token budget.""" + self.name = name + self.max_tokens = max_tokens +``` + +Add `Attributes:` only when a stored value differs in type or behaviour from the constructor +input (e.g. a `str` wrapped into a `CBlock`, or a class-level constant). +Pure-echo entries that repeat `Args:` verbatim should be omitted. + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for the full validation workflow. + --- ## Local preview From 3e52e64bcbd44944b2d49c1cfe4d4c38d2b7a263 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 13:28:55 +0000 Subject: [PATCH 13/14] docs: update nav with plugins/telemetry groups (#613) - docs/docs/docs.json: add plugins and telemetry/logging groups to API Reference nav (generated by pipeline run) - .gitignore: add .venv-docs-autogen/ (leftover from earlier failed run, no longer created by generate-ast.py) - docs/PR614-review-summary.md: add full review summary for agents and reviewers covering problem, Option C rationale, changes made, and known issues --- .gitignore | 1 + docs/docs/docs.json | 130 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 28c2508cc..6bc21d762 100644 --- a/.gitignore +++ b/.gitignore @@ -456,3 +456,4 @@ pyrightconfig.json # Generated API documentation (built by tooling/docs-autogen/) docs/docs/api/ docs/docs/api-reference.mdx +.venv-docs-autogen/ diff --git a/docs/docs/docs.json b/docs/docs/docs.json index af5aaa45c..5446e8516 100644 --- a/docs/docs/docs.json +++ b/docs/docs/docs.json @@ -262,6 +262,30 @@ "api/mellea/helpers/server_type" ] }, + { + "group": "plugins", + "pages": [ + "api/mellea/plugins/base", + "api/mellea/plugins/context", + "api/mellea/plugins/decorators", + "api/mellea/plugins/manager", + "api/mellea/plugins/plugins", + "api/mellea/plugins/pluginset", + "api/mellea/plugins/registry", + "api/mellea/plugins/types", + { + "group": "hooks", + "pages": [ + "api/mellea/plugins/hooks/component", + "api/mellea/plugins/hooks/generation", + "api/mellea/plugins/hooks/sampling", + "api/mellea/plugins/hooks/session", + "api/mellea/plugins/hooks/tool", + "api/mellea/plugins/hooks/validation" + ] + } + ] + }, { "group": "stdlib", "pages": [ @@ -344,6 +368,7 @@ "group": "telemetry", "pages": [ "api/mellea/telemetry/backend_instrumentation", + "api/mellea/telemetry/logging", "api/mellea/telemetry/metrics", "api/mellea/telemetry/telemetry", "api/mellea/telemetry/tracing" @@ -424,26 +449,89 @@ "thumbsRating": true }, "redirects": [ - { "source": "/overview/overview", "destination": "/getting-started/quickstart" }, - { "source": "/overview/mellea-welcome", "destination": "/concepts/generative-programming" }, - { "source": "/overview/generative-programming", "destination": "/concepts/generative-programming" }, - { "source": "/overview/architecture", "destination": "/guide/backends-and-configuration" }, - { "source": "/core-concept/instruct-validate-repair", "destination": "/concepts/instruct-validate-repair" }, - { "source": "/core-concept/requirements", "destination": "/concepts/requirements-system" }, - { "source": "/core-concept/generative-slots", "destination": "/guide/generative-functions" }, - { "source": "/core-concept/mobjects", "destination": "/concepts/mobjects-and-mify" }, - { "source": "/core-concept/agents", "destination": "/guide/tools-and-agents" }, - { "source": "/core-concept/context-management", "destination": "/how-to/use-context-and-sessions" }, - { "source": "/core-concept/alora", "destination": "/advanced/lora-and-alora-adapters" }, - { "source": "/core-concept/tuning", "destination": "/advanced/lora-and-alora-adapters" }, - { "source": "/core-concept/modeloptions", "destination": "/how-to/configure-model-options" }, - { "source": "/core-concept/interoperability", "destination": "/integrations/mcp" }, - { "source": "/integrations/mcp-and-m-serve", "destination": "/integrations/mcp" }, - { "source": "/core-concept/adapters", "destination": "/guide/tools-and-agents" }, - { "source": "/core-concept/contribution-guide", "destination": "/guide/CONTRIBUTING" }, - { "source": "/core-concept/prompt-engineering", "destination": "/advanced/mellea-core-internals" }, - { "source": "/integrations/bedrock-and-watsonx", "destination": "/integrations/bedrock" }, - { "source": "/integrations/huggingface-and-vllm", "destination": "/integrations/huggingface" }, - { "source": "/integrations/langchain-and-smolagents", "destination": "/integrations/langchain" } + { + "source": "/overview/overview", + "destination": "/getting-started/quickstart" + }, + { + "source": "/overview/mellea-welcome", + "destination": "/concepts/generative-programming" + }, + { + "source": "/overview/generative-programming", + "destination": "/concepts/generative-programming" + }, + { + "source": "/overview/architecture", + "destination": "/guide/backends-and-configuration" + }, + { + "source": "/core-concept/instruct-validate-repair", + "destination": "/concepts/instruct-validate-repair" + }, + { + "source": "/core-concept/requirements", + "destination": "/concepts/requirements-system" + }, + { + "source": "/core-concept/generative-slots", + "destination": "/guide/generative-functions" + }, + { + "source": "/core-concept/mobjects", + "destination": "/concepts/mobjects-and-mify" + }, + { + "source": "/core-concept/agents", + "destination": "/guide/tools-and-agents" + }, + { + "source": "/core-concept/context-management", + "destination": "/how-to/use-context-and-sessions" + }, + { + "source": "/core-concept/alora", + "destination": "/advanced/lora-and-alora-adapters" + }, + { + "source": "/core-concept/tuning", + "destination": "/advanced/lora-and-alora-adapters" + }, + { + "source": "/core-concept/modeloptions", + "destination": "/how-to/configure-model-options" + }, + { + "source": "/core-concept/interoperability", + "destination": "/integrations/mcp" + }, + { + "source": "/integrations/mcp-and-m-serve", + "destination": "/integrations/mcp" + }, + { + "source": "/core-concept/adapters", + "destination": "/guide/tools-and-agents" + }, + { + "source": "/core-concept/contribution-guide", + "destination": "/guide/CONTRIBUTING" + }, + { + "source": "/core-concept/prompt-engineering", + "destination": "/advanced/mellea-core-internals" + }, + { + "source": "/integrations/bedrock-and-watsonx", + "destination": "/integrations/bedrock" + }, + { + "source": "/integrations/huggingface-and-vllm", + "destination": "/integrations/huggingface" + }, + { + "source": "/integrations/langchain-and-smolagents", + "destination": "/integrations/langchain" + } ] } From ec4e507eec836543aa9bfa09c4e6e10da40041f5 Mon Sep 17 00:00:00 2001 From: Nigel Jones Date: Mon, 16 Mar 2026 13:42:41 +0000 Subject: [PATCH 14/14] fix: remove --no-venv arg from build.py calls to generate-ast.py generate-ast.py no longer accepts --no-venv (removed when the isolated venv was dropped in favour of sys.executable). build.py was still passing it, breaking the docs CI build. Signed-off-by: Nigel Jones --- tooling/docs-autogen/build.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tooling/docs-autogen/build.py b/tooling/docs-autogen/build.py index a8b926086..5a4853634 100755 --- a/tooling/docs-autogen/build.py +++ b/tooling/docs-autogen/build.py @@ -48,11 +48,6 @@ def main(): help="Source repo root to document (default: this repo). " "Use to generate docs for a different checkout, e.g. --source-dir ../mellea-b", ) - parser.add_argument( - "--no-venv", - action="store_true", - help="Skip venv creation (pass to generate-ast.py)", - ) parser.add_argument( "--skip-generation", action="store_true", help="Skip AST generation" ) @@ -79,7 +74,6 @@ def main(): str( output_dir.parent ), # generate-ast.py expects docs/docs, not docs/docs/api - "--no-venv", # Always use current environment "--source-dir", str(repo_root), ] @@ -126,7 +120,6 @@ def main(): str(script_dir / "generate-ast.py"), "--docs-root", str(output_dir.parent), - "--no-venv", "--nav-only", "--source-dir", str(repo_root),