fix(python_codegen): serialize dataclass instances via json.dumps default #104

Open
timur wants to merge 6 commits from fix/json-default-dataclasses into development
Owner

Emit _json_default helper that maps @dataclass instances to plain dicts via dataclasses.asdict, wired into _call via json.dumps(..., default=...).

Without this, callers passing typed records (e.g. Contact(...), Event(...)) into *_set would hit TypeError: Object of type Contact is not JSON serializable. The helper is always emitted — harmless when no dataclasses are declared — to keep the generated code uniform across services.

Verified by regenerating hero_osis_business_client.py and running scripts/e2e_typed_client.py in hero_logic — typed Opportunity dataclasses round-trip cleanly through opportunity.set / opportunity.get.

🤖 Generated with Claude Code

Emit `_json_default` helper that maps `@dataclass` instances to plain dicts via `dataclasses.asdict`, wired into `_call` via `json.dumps(..., default=...)`. Without this, callers passing typed records (e.g. `Contact(...)`, `Event(...)`) into `*_set` would hit `TypeError: Object of type Contact is not JSON serializable`. The helper is always emitted — harmless when no dataclasses are declared — to keep the generated code uniform across services. Verified by regenerating `hero_osis_business_client.py` and running `scripts/e2e_typed_client.py` in hero_logic — typed Opportunity dataclasses round-trip cleanly through opportunity.set / opportunity.get. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(python_codegen): serialize dataclass instances via json.dumps default
Some checks failed
Build & Test / check (pull_request) Failing after 35s
5ca8cff98a
Emit `_json_default` helper that maps `@dataclass` instances to plain
dicts via `dataclasses.asdict`, and wire it into `_call` as the
`json.dumps(..., default=...)` callback. Without this, callers passing
typed records (e.g. `Contact(...)`, `Event(...)`) into `*_set` would
hit `TypeError: Object of type Contact is not JSON serializable`.

The helper is always emitted — harmless when no dataclasses are
declared — to keep the generated code uniform across services.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(python_codegen): expose enum values + ref types in interface stub
Some checks failed
Build & Test / check (pull_request) Failing after 21s
32c2ed66ed
The interface stub the agent feeds to the code-gen LLM was hiding
critical schema information:

* `$ref` fields rendered as `Any (required)` because the data-structures
  pretty-printer only honored declared types, not refs. The model then
  passed `status="open"` against a TaskStatus whose real enum is
  ["backlog","todo","in_progress",...] and crashed on every attempt.
* Enum schemas (TaskStatus, TaskPriority, etc.) only emitted the title
  ("# TaskStatus: Task status") with no values — the LLM had nothing
  to ground the literal it was supposed to pick.
* Optional `$ref` dataclass fields wrapped to `Optional["X"]` even when
  the schema said the field was required, signalling false flexibility.

Fixes:
* Interface generator now renders enum schemas as
  `# TaskStatus: Task status — enum["backlog","todo",...]`.
* `$ref` fields show their target type name (`priority: TaskPriority`)
  instead of `Any`.
* Dataclass codegen distinguishes required vs optional `$ref` fields
  (`status: "TaskStatus"` vs `assignee_sid: Optional["..."]`).
* Sanitize dataclass docstrings: strip a trailing `"` so descriptions
  like `... not "issues"` don't collapse `"""...""""` into a Python
  SyntaxError (and an OTOML parse error when the script is later
  persisted in a span).

Verified by re-running the issues-bucketing prompt that previously
failed all 3 attempts: the model now uses valid status literals from
the enum and correctly imports the AI service. Remaining failures are
pure model-quality issues (e.g. forgetting one of two required fields)
rather than schema-blindness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(python_codegen): strip None-valued optional kwargs from wire payload
Some checks failed
Build & Test / check (pull_request) Failing after 35s
0121c7a7ac
`_call` was unconditionally serializing every method parameter into
the JSON dict — including optional ones whose default is `None`.
Upstream JSON-RPC servers and HTTP APIs that forward the dict to a
third party (e.g. aibroker → OpenAI) reject explicit nulls with
errors like `property 'system' is unsupported, type:
invalid_request_error`, even when the caller never set the kwarg.

Required params still ship verbatim. Optional params are now added
to the wire dict only when the caller passed a non-None value:

    _params = {"model": model, "messages": messages}
    if system is not None: _params["system"] = system
    if top_p is not None: _params["top_p"] = top_p
    ...

This matches what a Python user would write by hand and matches the
behaviour of OpenAI/OpenRouter SDK clients.

Verified: hero_aibroker_client.py now sends {"model","messages",...}
without the long tail of None kwargs, and the typed-client e2e test
still passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(python_codegen): emit health() helper on every generated client
Some checks failed
Build & Test / check (pull_request) Failing after 38s
5df690d0c7
Per the hero_sockets spec every Hero socket exposes `GET /health`,
but the generated Python clients only had wrappers for the methods
declared in the OpenRPC spec. Callers (and the service_agent's
pre-flight `check_service_health` step in hero_logic) had no way to
probe the service without dropping into raw HTTP.

Emit a generic `client.health() -> dict` that hits `GET /health`
over the same UDS connection used for RPC. Returns the parsed JSON
when the server complies, or `{"status": "ok"|"unknown", "raw": ...}`
when it doesn't. No spec coverage required — every socket has it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(python_codegen): annotate dataclass vs dict returns in interface stub
Some checks failed
Build & Test / check (pull_request) Failing after 37s
fd1d0ee688
The interface comment line emitted for each method only said
`-> Recipe` or `-> dict`, leaving the LLM reading the stub to guess
whether the result wanted attribute access or subscript access.
That ambiguity was the source of every "TypeError: 'Foo' object is
not subscriptable" / "AttributeError: 'dict' object has no
attribute 'content'" we saw across the seeded examples.

Annotate the return type:

    -> Task [dataclass: use .field]
    -> Board [dataclass: use .field]
    -> {field, ...} [dict: use record["key"]]
    -> list[Task] [each item is a dataclass: use item.field]

The bracket suffix is the cheapest possible nudge — every method
line now tells the model exactly which accessor to use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(scanner): walk one level deeper for composite-service sub-sockets
Some checks failed
Build & Test / check (pull_request) Failing after 32s
1a73c708cc
hero_sockets composite services (e.g. hero_aibroker) split their RPC
surface into per-domain subdirectories:

    hero_aibroker/chat/rpc.sock
    hero_aibroker/models/rpc.sock
    hero_aibroker/embedder/rpc.sock
    …

The scanner only walked two levels (`socket_dir/<svc>/<socket>.sock`)
so every composite sub-service was invisible — the catalog had a
phantom top-level `hero_aibroker/rpc.sock` (stale) and nothing else
once the file was deleted on restart.

Detect a sub-directory under a service dir, treat it as a sub-service
named `<parent>_<child>` (matching the documented composite-service
convention in the hero_sockets skill), and probe the sockets inside.
After this, the catalog grows entries like:

    35 methods  hero_aibroker_admin
     7 methods  hero_aibroker_chat
    13 methods  hero_aibroker_models
    11 methods  hero_aibroker_memory
    …

so the agent can address `hero_aibroker_chat.ai_chat(...)` instead
of guessing at a non-existent top-level socket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some checks failed
Build & Test / check (pull_request) Failing after 32s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/json-default-dataclasses:fix/json-default-dataclasses
git switch fix/json-default-dataclasses

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch development
git merge --no-ff fix/json-default-dataclasses
git switch fix/json-default-dataclasses
git rebase development
git switch development
git merge --ff-only fix/json-default-dataclasses
git switch fix/json-default-dataclasses
git rebase development
git switch development
git merge --no-ff fix/json-default-dataclasses
git switch development
git merge --squash fix/json-default-dataclasses
git switch development
git merge --ff-only fix/json-default-dataclasses
git switch development
git merge fix/json-default-dataclasses
git push origin development
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_router!104
No description provided.