Skip to content

Comments

Update haiway attributes#427

Merged
KaQuMiQ merged 1 commit intomainfrom
feature/haiway_attributes
Sep 26, 2025
Merged

Update haiway attributes#427
KaQuMiQ merged 1 commit intomainfrom
feature/haiway_attributes

Conversation

@KaQuMiQ
Copy link
Collaborator

@KaQuMiQ KaQuMiQ commented Sep 26, 2025

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Sep 26, 2025

Warning

Rate limit exceeded

@KaQuMiQ has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 26 minutes and 1 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between fc317ec and ae6984f.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • Makefile (1 hunks)
  • pyproject.toml (2 hunks)
  • src/draive/mcp/client.py (2 hunks)
  • src/draive/openai/responses.py (7 hunks)
  • src/draive/openai/utils.py (1 hunks)
  • src/draive/parameters/__init__.py (0 hunks)
  • src/draive/parameters/coding.py (1 hunks)
  • src/draive/parameters/function.py (5 hunks)
  • src/draive/parameters/model.py (18 hunks)
  • src/draive/parameters/parameter.py (2 hunks)
  • src/draive/parameters/specification.py (13 hunks)
  • src/draive/parameters/validation.py (3 hunks)
  • src/draive/resources/template.py (3 hunks)
  • src/draive/vllm/messages.py (8 hunks)
  • src/draive/vllm/utils.py (1 hunks)
  • tests/test_model.py (2 hunks)
  • tests/test_specification.py (2 hunks)
  • tests/test_tags_extraction.py (1 hunks)

Walkthrough

This PR updates build/config (Makefile: remove .env include; UV_VERSION 0.8.22) and bumps package/dependency versions (pyproject: 0.86.0; haiway ~=0.34.0). It replaces OpenAI/vLLM missing-value sentinels from NOT_GIVEN/NotGiven to Omit/omit across modules. Parameters subsystem undergoes major refactor: new ParametersJSONEncoder; validation API changes (validated→validate; ParameterValidator signatures); specification dispatch via AttributeAnnotation types; DataModel redesigned around FIELDS, serialization, and generics handling. MCP client switches to ToolParametersSpecification. ResourceTemplate gains URI parameter coercion per ParameterSpecification. Tests updated to match new specification/streaming behaviors.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Cleanup #414 — Also alters mcp/client tool parameter wiring toward typed ToolParametersSpecification, overlapping this PR’s client-side tool conversion changes.
  • Switch to non-strict OpenAI tools #412 — Refactors tool-parameter/specification machinery, mirroring this PR’s move away from validated_tool_specification to ToolParametersSpecification.
  • Rework realtime sessions #338 — Changes FunctionTool parameter handling and specification construction, closely related to this PR’s tool parameter API updates.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “Update haiway attributes” focuses narrowly on a dependency change for haiway but does not reflect the extensive refactoring across parameter validation, DataModel redesign, sentinel handling (Omit/omit), JSON encoding, OpenAI and vllm integrations, version bumps, and other core API alterations. It fails to summarize the primary scope or intent of the changeset, making it unclear to reviewers what the main impact of the pull request is. Please revise the title to concisely convey the main objective of the pull request, for example “Refactor parameter validation and adopt Omit sentinel across OpenAI and vllm integrations” or another phrase that captures the core refactoring and API changes.
Description Check ❓ Inconclusive No description was provided for the pull request, leaving reviewers without any context or summary of the changes and objectives. This absence of information is too vague to assess the relevance of the changes. Add a descriptive summary of the pull request that outlines its scope, key refactorings, and intended impact so that reviewers can quickly understand the purpose and rationale behind the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
src/draive/parameters/function.py (2)

282-285: Fix TypeError formatting (currently creates a tuple message).

Use an f-string or % formatting to avoid a tuple in the exception args.

-        raise TypeError(
-            "Untyped argument %s",
-            parameter.name,
-        )
+        raise TypeError(f"Untyped argument {parameter.name}")

281-294: Preserve Annotated metadata by adding include_extras=True
Replace the lone call in src/draive/parameters/function.py (line 187)

- type_hints: Mapping[str, Any] = get_type_hints(function)
+ type_hints: Mapping[str, Any] = get_type_hints(function, include_extras=True)
src/draive/resources/template.py (1)

279-307: Fix parameter extraction regex: wrong group order, missing escaping, and single-segment slash capture.

Current approach can mis-map values when {/var} and {var} are interleaved, treats {/var} as a single segment, and doesn’t escape literal regex metacharacters (e.g., dots). Build the regex in one pass, preserve parameter order, escape literals, and capture multi-segment slash expansions.

Apply this diff:

-    def _extract_path_parameters_with_slash(
-        self,
-        actual_path: str,
-    ) -> dict[str, str]:
-        params: dict[str, str] = {}
-        slash_params: list[Any] = re.findall(r"\{/([^}]+)\}", self.declaration.template_uri)
-        path_template: str = self._extract_path_template()
-
-        # Create regex pattern
-        template_for_regex: str = path_template
-        for param in slash_params:
-            template_for_regex = template_for_regex.replace(f"{{/{param}}}", "/([^/]+)")
-
-        # Handle regular {var} patterns
-        regular_params: list[Any] = re.findall(r"\{([^}]+)\}", template_for_regex)
-        for param in regular_params:
-            if not param.startswith("/"):  # Skip already handled slash patterns
-                template_for_regex = template_for_regex.replace(f"{{{param}}}", "([^/]+)")
-
-        # Match against the actual path
-        match: re.Match[str] | None = re.match(f"^{template_for_regex}$", actual_path)
-        if match:
-            all_params: list[Any] = slash_params + [
-                p for p in regular_params if not p.startswith("/")
-            ]
-            for param, value in zip(all_params, match.groups(), strict=False):
-                params[param] = value
-
-        return params
+    def _extract_path_parameters_with_slash(
+        self,
+        actual_path: str,
+    ) -> dict[str, str]:
+        # Build a safe regex and preserve parameter order
+        path_template: str = self._extract_path_template()
+        pattern_parts: list[str] = []
+        param_names: list[str] = []
+
+        idx = 0
+        for m in re.finditer(r"\{(/)?([^}]+)\}", path_template):
+            # Literal part before the placeholder
+            if m.start() > idx:
+                pattern_parts.append(re.escape(path_template[idx : m.start()]))
+
+            slash_prefix, name = m.groups()
+            param_names.append(name)
+            if slash_prefix:
+                # Capture one-or-more slash-delimited segments
+                pattern_parts.append(r"((?:/[^/?#]+)+)")
+            else:
+                # Capture a single path segment
+                pattern_parts.append(r"([^/?#]+)")
+
+            idx = m.end()
+
+        # Trailing literal
+        if idx < len(path_template):
+            pattern_parts.append(re.escape(path_template[idx:]))
+
+        pattern = "^" + "".join(pattern_parts) + "$"
+        m = re.match(pattern, actual_path)
+        if not m:
+            return {}
+
+        # Map captured groups back to parameter names in order
+        params: dict[str, str] = {}
+        for name, value in zip(param_names, m.groups(), strict=False):
+            # Remove the leading slash for {/var} captures
+            params[name] = value.lstrip("/")
+
+        return params
src/draive/parameters/specification.py (1)

446-456: Do not instantiate TypedDict classes at runtime

TypedDict types aren’t callable; returning ParameterAlternativesSpecification(...) will raise. Return plain dicts instead.

-            return ParameterAlternativesSpecification(
-                type=compressed_alternatives,
-                description=description,
-            )
+            return {
+                "type": compressed_alternatives,
+                "description": description,
+            }
@@
-            return ParameterAlternativesSpecification(
-                type=compressed_alternatives,
-            )
+            return {
+                "type": compressed_alternatives,
+            }

Also applies to: 461-465

src/draive/parameters/model.py (3)

444-446: PEP 695 generics: avoid requiring typing.Generic in bases

Classes using the new generic syntax don’t inherit from typing.Generic; this assert will fail for e.g., class XT. Check type_params instead.

-        assert Generic in cls.__bases__, "Can't specialize non generic type!"  # nosec: B101
+        assert getattr(cls, "__type_params__", None) is not None, "Can't specialize non-generic type!"  # nosec: B101

633-683: vars(self) on slotted classes raises; adapt for slots

For slotted DataModel subclasses without dict, vars(self) raises TypeError. Use FIELDS for membership, iteration, and length.

     def __contains__(
         self,
         element: Any,
     ) -> bool:
-        return element in vars(self)
+        if getattr(self, "__slots__", ()):
+            return isinstance(element, str) and any(element == f.name for f in self.__FIELDS__)
+        else:
+            return element in vars(self)
@@
     def __iter__(self) -> Iterator[str]:
-        yield from vars(self)
+        if getattr(self, "__slots__", ()):
+            yield from (f.name for f in self.__FIELDS__)
+        else:
+            yield from vars(self)
@@
     def __len__(self) -> int:
-        return len(vars(self))
+        if getattr(self, "__slots__", ()):
+            return len(self.__FIELDS__)
+        else:
+            return len(vars(self))

41-44: Add docstrings for Field and DataModel

These are public APIs; add concise NumPy-style docstrings.

As per coding guidelines

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ba2ebd and fc317ec.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • Makefile (1 hunks)
  • pyproject.toml (2 hunks)
  • src/draive/mcp/client.py (2 hunks)
  • src/draive/openai/responses.py (7 hunks)
  • src/draive/openai/utils.py (1 hunks)
  • src/draive/parameters/__init__.py (0 hunks)
  • src/draive/parameters/coding.py (1 hunks)
  • src/draive/parameters/function.py (5 hunks)
  • src/draive/parameters/model.py (18 hunks)
  • src/draive/parameters/parameter.py (2 hunks)
  • src/draive/parameters/specification.py (13 hunks)
  • src/draive/parameters/validation.py (3 hunks)
  • src/draive/resources/template.py (3 hunks)
  • src/draive/vllm/messages.py (8 hunks)
  • src/draive/vllm/utils.py (1 hunks)
  • tests/test_model.py (2 hunks)
  • tests/test_specification.py (2 hunks)
  • tests/test_tags_extraction.py (1 hunks)
💤 Files with no reviewable changes (1)
  • src/draive/parameters/init.py
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use absolute imports from the draive package (e.g., from draive import ...), not relative imports
Follow Ruff import ordering: standard library, third-party, then local imports
Use Python 3.12+ typing features (e.g., | unions, PEP 695 generics)
Prefer abstract/base collection types (Sequence, Mapping, Set, Iterable) over concrete list, dict, set in type hints
Define and raise custom exception types for specific error cases
Use NumPy docstring convention for all functions, classes, and methods
Skip module-level docstrings unless explicitly requested
Docstrings should include Parameters, Returns, Raises, and Notes (if needed) sections

Format all Python code with Ruff formatter (run via make format); do not use any other formatter

Files:

  • src/draive/openai/utils.py
  • tests/test_model.py
  • tests/test_tags_extraction.py
  • src/draive/vllm/utils.py
  • src/draive/vllm/messages.py
  • src/draive/resources/template.py
  • tests/test_specification.py
  • src/draive/parameters/specification.py
  • src/draive/parameters/validation.py
  • src/draive/parameters/coding.py
  • src/draive/openai/responses.py
  • src/draive/mcp/client.py
  • src/draive/parameters/function.py
  • src/draive/parameters/parameter.py
  • src/draive/parameters/model.py
src/draive/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/draive/**/*.py: Import Haiway symbols directly as from haiway import State, ctx
Use ctx.scope(...) to bind active State instances; avoid global state
Route all logging through ctx.log_* and never use print
Enforce strict typing on public APIs; avoid untyped definitions and Any except at verified third‑party boundaries
Prefer explicit attribute access with static types; avoid dynamic getattr except at narrow boundaries
Use abstract immutable protocols (Mapping, Sequence, Iterable) instead of concrete dict/list/set in public types
Use final where applicable; prefer composition over complex inheritance
Use precise unions (|), narrow with match/isinstance, and avoid cast unless provably safe and localized
Model immutable configuration/data as haiway.State; avoid in‑place mutation and prefer State.updated(...)/functional builders
Access active state via haiway.ctx within async scopes (ctx.scope(...))
Public state methods that dispatch on the active instance must use @statemethod
Add structured, concise logs around generation calls, tool dispatch, and provider requests/responses (without secrets); add metrics via ctx.record where applicable
All I/O is async; keep boundaries async, use ctx.spawn for detached tasks, and avoid custom threading
Translate provider/SDK errors into typed exceptions; never raise bare Exception; preserve context
Compose multimodal content with MultimodalContent.of(...); use ResourceContent/Reference for blobs; wrap custom data in ArtifactContent (use hidden when needed)
Public symbols require NumPy-style docstrings with Parameters/Returns/Raises and rationale when non-obvious
Avoid docstrings for internal helpers; prefer self-explanatory names; skip module-level docstrings
Never log secrets or full request bodies containing keys/tokens
Read credentials from environment variables; resolve via helpers like getenv_str

Files:

  • src/draive/openai/utils.py
  • src/draive/vllm/utils.py
  • src/draive/vllm/messages.py
  • src/draive/resources/template.py
  • src/draive/parameters/specification.py
  • src/draive/parameters/validation.py
  • src/draive/parameters/coding.py
  • src/draive/openai/responses.py
  • src/draive/mcp/client.py
  • src/draive/parameters/function.py
  • src/draive/parameters/parameter.py
  • src/draive/parameters/model.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Place tests in the tests/ directory and use pytest (with async support)
Mark asynchronous tests with pytest.mark.asyncio
Mock dependencies within test scope using stubbed functionality state (e.g., via ctx.scope)

tests/**/*.py: Unit tests must not perform network I/O; mock providers/HTTP
Use pytest-asyncio (@pytest.mark.asyncio) for coroutine tests
Prefer scoping with ctx.scope(...) in tests and bind required State instances explicitly
Avoid real I/O in tests; stub provider calls and HTTP

Files:

  • tests/test_model.py
  • tests/test_tags_extraction.py
  • tests/test_specification.py
🧠 Learnings (1)
📚 Learning: 2025-09-11T15:22:15.967Z
Learnt from: CR
PR: miquido/draive#0
File: AGENTS.md:0-0
Timestamp: 2025-09-11T15:22:15.967Z
Learning: Applies to src/draive/__init__.py : Centralize and update all public exports in `src/draive/__init__.py` when API surface changes

Applied to files:

  • src/draive/parameters/specification.py
🧬 Code graph analysis (11)
src/draive/openai/utils.py (2)
src/draive/vllm/utils.py (1)
  • unwrap_missing (9-17)
src/draive/gemini/utils.py (5)
  • unwrap_missing (10-14)
  • unwrap_missing (18-22)
  • unwrap_missing (26-32)
  • unwrap_missing (36-42)
  • unwrap_missing (45-59)
tests/test_tags_extraction.py (1)
src/draive/multimodal/content.py (4)
  • tags (427-446)
  • MultimodalContent (25-592)
  • of (42-66)
  • of (619-647)
src/draive/vllm/messages.py (2)
src/draive/openai/utils.py (1)
  • unwrap_missing (9-17)
src/draive/vllm/utils.py (1)
  • unwrap_missing (9-17)
src/draive/resources/template.py (2)
src/draive/parameters/parameter.py (2)
  • Parameter (14-143)
  • find (80-98)
src/draive/parameters/function.py (1)
  • ParametrizedFunction (136-271)
tests/test_specification.py (1)
src/draive/parameters/specification.py (1)
  • parameter_specification (204-229)
src/draive/parameters/validation.py (2)
src/draive/parameters/parameter.py (2)
  • of (16-55)
  • validate (127-143)
src/draive/parameters/model.py (3)
  • validate (323-326)
  • validate (521-533)
  • to_mapping (546-554)
src/draive/openai/responses.py (2)
src/draive/openai/utils.py (1)
  • unwrap_missing (9-17)
src/draive/vllm/utils.py (1)
  • unwrap_missing (9-17)
src/draive/mcp/client.py (2)
src/draive/models/tools/types.py (1)
  • parameters (79-79)
src/draive/parameters/specification.py (1)
  • ToolParametersSpecification (190-201)
src/draive/parameters/function.py (1)
src/draive/parameters/parameter.py (3)
  • validate (127-143)
  • find (80-98)
  • pick (100-125)
src/draive/parameters/parameter.py (1)
src/draive/parameters/model.py (2)
  • validate (323-326)
  • validate (521-533)
src/draive/parameters/model.py (3)
src/draive/parameters/coding.py (2)
  • ParametersJSONEncoder (11-26)
  • default (12-26)
src/draive/parameters/parameter.py (4)
  • Parameter (14-143)
  • of (16-55)
  • validate (127-143)
  • find (80-98)
src/draive/parameters/validation.py (3)
  • of (36-65)
  • validator (57-60)
  • validator (94-105)
🔇 Additional comments (25)
pyproject.toml (1)

8-24: Version bump to match dependency upgrade is sound

The project version and haiway constraint move forward together, which keeps packaging consistent with the dependency upgrade. No issues spotted.

Makefile (1)

13-13: UV toolchain update looks good

Bumping the pinned UV version to 0.8.22 keeps the bootstrap script current and the version gate still works with the existing comparison logic.

src/draive/vllm/utils.py (1)

4-4: LGTM: sentinel migration to Omit/omit is correct.

Import and usage align with the updated OpenAI SDK semantics.

src/draive/parameters/function.py (1)

227-243: Rename to Parameter.validate: good migration.

Swapping validated→validate matches the new Parameter API and keeps error scoping under ValidationContext.

src/draive/vllm/messages.py (4)

171-187: Response format: omit semantics look correct.

Type hint and defaulting to omit align with SDK expectations; json/json_schema branches preserved.


204-206: Conditional omission for optional params is correct.

Using omit for parallel_tool_calls and stop when absent prevents sending unwanted keys.

Also applies to: 211-213, 356-358, 363-364


625-633: Return (omit, omit) when no tools.

Correctly omits both tool_choice and tools for empty tool sets; signatures updated accordingly.


8-8: Confirmed: pyproject.toml pins openai~=1.104, which includes Omit/omit support.

src/draive/parameters/validation.py (2)

43-66: Validator composition is sound.

Wrapping base validator for object-attribute handling and layering verifier preserves error locality and typing.


83-107: Smart coercion for DataModel generics.

The origin/parameters check with to_mapping fallback is a pragmatic solution to reconcile mismatched generic parameters.

Also applies to: 110-121, 137-158

tests/test_tags_extraction.py (1)

78-79: LGTM: formatting-only simplification.

Single-line construction preserves behavior.

src/draive/parameters/coding.py (1)

11-26: Encoder logic is correct and safe.

Order of isinstance checks avoids datetime-as-date pitfalls; UUID/datetime/date/time encodings are appropriate. LGTM.

tests/test_model.py (2)

521-528: Streaming iterator path is clear and deterministic.

Yielding text, then image, then audio simplifies expectations for the selection layer. LGTM.


543-543: Concise async collection.

The single-line async comprehension is fine and equivalent to the prior multi-step approach.

src/draive/openai/responses.py (3)

463-491: Mirror check for stream path unwrap_missing + omit usage.

Same potential signature issue as above in the streaming call. If unwrap_missing doesn’t accept default, remove the argument and rely on its built-in omit behavior.

Based on learnings

Use the same script as above to verify.


658-666: Text output omission when output='auto' is correct.

Returning omit here aligns with the SDK’s sentinel expectations and avoids sending redundant defaults.


217-246: Verify haiway.unwrap_missing supports default parameter
unwrap_missing(config.temperature, default=omit) may not accept a default argument—confirm its signature in the haiway package and adjust accordingly.

src/draive/parameters/parameter.py (2)

4-4: Imports align with the updated validation API.

Switch to AttributeAnnotation and Validator is consistent with the broader refactor.


127-144: Rename to validate and streamlined logic looks good.

Validation scoping and default handling are sound; the API now matches ParametrizedFunction.validate_arguments.

src/draive/resources/template.py (2)

351-367: Coercion pipeline for URI params is solid.

Mapping raw URI strings into typed kwargs using specification-driven coercion is a good approach and remains a no-op for unknown keys. LGTM.


525-540: Happy path for resolve_from_uri reads cleanly.

Pre-coercion + validated call + proper error wrapping into ResourceCorrupted is correct.

src/draive/mcp/client.py (1)

31-31: Confirm public export path for DataModel

Import switched to draive.parameters. Ensure DataModel remains re-exported from draive/init.py to keep the stable top-level API, or import from draive for consistency.

Based on learnings

tests/test_specification.py (1)

21-101: LGTM: attribute-based specifications and compressed unions

The move to dedicated Attribute types and compressed union types is coherent and matches the new specification dispatch.

src/draive/parameters/specification.py (1)

536-573: Enum handling looks good

String and integer enum specifications correctly populate enum arrays and optional descriptions.

src/draive/openai/utils.py (1)

4-4: Add docstring and confirm SDK supports Omit/omit

Apply this NumPy-style docstring diff to unwrap_missing (lines 12–15):

 def unwrap_missing[Value](
     value: Value | Missing,
     /,
 ) -> Value | Omit:
+    """Return omit for MISSING; otherwise return the original value.
+
+    Parameters
+    ----------
+    value : Value | Missing
+        Value to unwrap.
+
+    Returns
+    -------
+    Value | Omit
+        omit when value is MISSING; otherwise the original value.
+    """
     if value is MISSING:
         return omit

Confirm that the pinned openai~=1.104 exposes Omit and omit; bump the version if it doesn’t.

Comment on lines +483 to 489
parameters=cast(
ToolParametersSpecification,
{
**mcp_tool.inputSchema,
"additionalProperties": False,
}
},
),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Validate/normalize MCP tool inputSchema instead of blind cast

mcp_tool.inputSchema can be None or a non-object schema. Spreading it and casting will raise at runtime and/or violate ToolParametersSpecification invariants (type must be "object"). Build a safe, strict object schema and propagate optional fields.

Apply this diff in-place:

-        parameters=cast(
-            ToolParametersSpecification,
-            {
-                **mcp_tool.inputSchema,
-                "additionalProperties": False,
-            },
-        ),
+        parameters=_coerce_tool_parameters_spec(mcp_tool.inputSchema),

Add this helper (outside the shown range, e.g., below _convert_tool or near module top):

def _coerce_tool_parameters_spec(
    input_schema: Mapping[str, Any] | None,
) -> ToolParametersSpecification:
    if not isinstance(input_schema, Mapping):
        ctx.log_warning("Missing or invalid MCP tool inputSchema; defaulting to strict object")
        return {
            "type": "object",
            "properties": {},
            "additionalProperties": False,
        }

    type_value = input_schema.get("type", "object")
    if type_value != "object":
        ctx.log_warning(f"Unexpected MCP tool inputSchema.type={type_value!r}; forcing 'object'")

    spec: ToolParametersSpecification = {
        "type": "object",
        "properties": cast(Mapping[str, Any], input_schema.get("properties", {})),
        "additionalProperties": False,
    }

    required = input_schema.get("required")
    if isinstance(required, Sequence):
        spec["required"] = [str(k) for k in required]  # type: ignore[assignment]

    title = input_schema.get("title")
    if isinstance(title, str):
        spec["title"] = title  # type: ignore[assignment]

    description = input_schema.get("description")
    if isinstance(description, str):
        spec["description"] = description  # type: ignore[assignment]

    return spec
🤖 Prompt for AI Agents
In src/draive/mcp/client.py around lines 483 to 489, the code blindly casts
mcp_tool.inputSchema into ToolParametersSpecification which can be None or a
non-object schema; create a helper _coerce_tool_parameters_spec(input_schema) as
described and use it instead of the cast so you always produce a strict object
schema (type "object", properties from input if mapping, additionalProperties
False) while preserving optional fields title, description, and required (coerce
required to strings if sequence), and emit ctx.log_warning when input_schema is
missing/invalid or its type is not "object"; place the helper near the module
top or below _convert_tool and replace the current parameters=cast(...) call
with parameters=_coerce_tool_parameters_spec(mcp_tool.inputSchema).

Comment on lines +204 to 229
def parameter_specification(
annotation: AttributeAnnotation,
/,
description: str | None,
) -> ParameterSpecification:
if specification := SPECIFICATIONS.get(annotation.origin):
if isinstance(annotation, AliasAttribute):
return parameter_specification(annotation.resolved, description)

if specification := SPECIFICATIONS.get(type(annotation)):
return specification(annotation, description)

elif hasattr(annotation.origin, "__PARAMETERS_SPECIFICATION__"):
elif hasattr(annotation.base, "__PARAMETERS_SPECIFICATION__"):
if description := description:
return cast(
ParameterSpecification,
{
**annotation.origin.__PARAMETERS_SPECIFICATION__,
**annotation.base.__PARAMETERS_SPECIFICATION__,
"description": description,
},
)

else:
return annotation.origin.__PARAMETERS_SPECIFICATION__

elif is_typeddict(annotation.origin):
return _prepare_specification_of_typed_dict(
annotation,
description=description,
)

elif issubclass(annotation.origin, IntEnum):
if description := description:
return {
"type": "integer",
"enum": list(annotation.origin),
"description": description,
}

else:
return {
"type": "integer",
"enum": list(annotation.origin),
}

elif issubclass(annotation.origin, StrEnum):
if description := description:
return {
"type": "string",
"enum": list(annotation.origin),
"description": description,
}

else:
return {
"type": "string",
"enum": list(annotation.origin),
}
return annotation.base.__PARAMETERS_SPECIFICATION__

else:
raise TypeError(f"Unsupported type annotation: {annotation}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add docstring to parameter_specification

Public API should have NumPy-style docstrings per guidelines.

As per coding guidelines

🤖 Prompt for AI Agents
In src/draive/parameters/specification.py around lines 204 to 229, add a
NumPy-style docstring to the public function parameter_specification; include a
short summary line, parameter entries for "annotation" (type
AttributeAnnotation) and "description" (str | None), a "Returns" section
describing the returned ParameterSpecification, a "Raises" section documenting
the TypeError for unsupported annotations, and any pertinent notes about alias
resolution or lookup behavior (e.g., that AliasAttribute is resolved and
SPECIFICATIONS and __PARAMETERS_SPECIFICATION__ are consulted); ensure
formatting follows the project's NumPy-style docstring conventions.

Comment on lines +22 to 33
return cast(
Validator[Dict],
cls.of(
resolve_attribute(
typed_dict,
module=typed_dict.__module__,
resolved_parameters={}, # TODO: type parameters?
recursion_guard={},
),
verifier=None,
),
verifier=None,
recursion_guard={},
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

TypedDict path uses resolve_attribute correctly; track TODO.

The recursion inputs look fine; the TODO about type parameters is noted.

I can help add propagation of resolved type parameters if you provide expected shapes/usages. Should I open a follow-up issue to track this?

🤖 Prompt for AI Agents
In src/draive/parameters/validation.py around lines 22 to 33, update the
resolve_attribute call so it forwards the current resolved type parameters
instead of passing an empty dict for resolved_parameters; identify where the
type parameter mappings are tracked in the calling context (or the
Validator/Class that invoked this) and pass that mapping through, preserve and
propagate recursion_guard as-is, and ensure subsequent recursive calls also
accept and forward the resolved_parameters mapping; if the expected shape/source
of the type-parameter mapping is unclear, create a follow-up issue documenting
the required mapping shape and where it should originate so the TODO can be
implemented consistently.

Comment on lines +379 to +440
def _coerce_from_specification( # noqa: C901, PLR0911, PLR0912
self,
specification: ParameterSpecification,
value: str,
) -> tuple[Any, bool]:
if isinstance(specification, Mapping) and "oneOf" in specification:
for option in specification["oneOf"]:
converted, changed = self._coerce_from_specification(option, value)
if changed:
return converted, True

return value, False

if isinstance(specification, Mapping):
type_name: Any | None = specification.get("type")
if type_name == "integer":
try:
return int(value), True
except ValueError:
return value, False

if type_name == "number":
try:
return float(value), True
except ValueError:
return value, False

if type_name == "boolean":
parsed_bool = self._parse_boolean(value)
if parsed_bool is not None:
return parsed_bool, True

return value, False

if type_name == "array":
parsed_array = self._parse_json_sequence(value)
if parsed_array is not None:
return parsed_array, True

return value, False

if type_name == "object":
parsed_object = self._parse_json_mapping(value)
if parsed_object is not None:
return parsed_object, True

return value, False

if type_name == "null":
normalised = value.strip().lower()
if normalised in {"null", "none"}:
return None, True

return value, False

if "enum" in specification:
converted_enum, enum_changed = self._match_enum(specification["enum"], value)
if enum_changed:
return converted_enum, True

return value, False

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Optional: Normalize specification to a mapping to harden coercion.

If ParameterSpecification isn’t always a Mapping, consider normalizing once with as_dict(specification) to ensure consistent key access across providers/versions.

As per coding guidelines

🤖 Prompt for AI Agents
In src/draive/resources/template.py around lines 379 to 440, the method
_coerce_from_specification assumes ParameterSpecification is a Mapping;
normalize the incoming specification once at the top by converting it to a dict
(e.g. spec = as_dict(specification) or similar helper) and then use spec for all
key lookups (spec.get("oneOf"), spec.get("type"), "enum" in spec, etc.),
replacing the repeated isinstance(...) checks so the function consistently
accesses keys from the normalized mapping and remains robust across
provider/version variations.

Comment on lines +9 to 17
def unwrap_missing[Value](
value: Value | Missing,
/,
) -> Value | NotGiven:
) -> Value | Omit:
if value is MISSING:
return NOT_GIVEN
return omit

else:
return cast(Value, value)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add a NumPy-style docstring for public API.

unwrap_missing is exported via all; per guidelines, public functions require NumPy-style docstrings.

As per coding guidelines

 def unwrap_missing[Value](
     value: Value | Missing,
     /,
 ) -> Value | Omit:
+    """
+    Unwrap haiway.MISSING into the OpenAI omit sentinel.
+
+    Parameters
+    ----------
+    value : Value | Missing
+        Input value or the MISSING sentinel.
+
+    Returns
+    -------
+    Value | Omit
+        The input value when present; otherwise the omit sentinel.
+    """
     if value is MISSING:
         return omit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def unwrap_missing[Value](
value: Value | Missing,
/,
) -> Value | NotGiven:
) -> Value | Omit:
if value is MISSING:
return NOT_GIVEN
return omit
else:
return cast(Value, value)
def unwrap_missing[Value](
value: Value | Missing,
/,
) -> Value | Omit:
"""
Unwrap haiway.MISSING into the OpenAI omit sentinel.
Parameters
----------
value : Value | Missing
Input value or the MISSING sentinel.
Returns
-------
Value | Omit
The input value when present; otherwise the omit sentinel.
"""
if value is MISSING:
return omit
else:
return cast(Value, value)
🤖 Prompt for AI Agents
In src/draive/vllm/utils.py around lines 9 to 17, the public function
unwrap_missing (exported via __all__) lacks a NumPy-style docstring; add a clear
NumPy-style docstring immediately above the function that briefly describes what
unwrap_missing does, documents the parameters (value: Value | Missing) and their
meaning, the return value (Value | Omit) and when omit is returned, and any side
effects or notes (e.g., that MISSING yields omit); keep wording concise and
include types under Parameters and Returns sections to satisfy the public API
guidelines.

Comment on lines +77 to 82
SequenceAttribute(
base=Sequence,
values=StringAttribute(),
),
description=None,
) == {"type": "array", "items": {"type": "string"}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove duplicated sequence spec assertion

This block duplicates the earlier SequenceAttribute(string) assertion; keep one to reduce noise.

-    assert parameter_specification(
-        SequenceAttribute(
-            base=Sequence,
-            values=StringAttribute(),
-        ),
-        description=None,
-    ) == {"type": "array", "items": {"type": "string"}}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
SequenceAttribute(
base=Sequence,
values=StringAttribute(),
),
description=None,
) == {"type": "array", "items": {"type": "string"}}
🤖 Prompt for AI Agents
In tests/test_specification.py around lines 77 to 82, there is a duplicated
assertion checking SequenceAttribute(base=Sequence, values=StringAttribute(),
description=None) == {"type": "array", "items": {"type": "string"}}; remove this
duplicate assertion so only the original SequenceAttribute(string) assertion
remains to avoid redundant tests and noise.

@KaQuMiQ KaQuMiQ force-pushed the feature/haiway_attributes branch from fc317ec to ae6984f Compare September 26, 2025 11:52
@KaQuMiQ KaQuMiQ merged commit 820cbef into main Sep 26, 2025
4 of 5 checks passed
@KaQuMiQ KaQuMiQ deleted the feature/haiway_attributes branch September 26, 2025 11:54
This was referenced Sep 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant