fix(python_codegen): serialize dataclass instances via json.dumps default #104
No reviewers
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_router!104
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/json-default-dataclasses"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Emit
_json_defaulthelper that maps@dataclassinstances to plain dicts viadataclasses.asdict, wired into_callviajson.dumps(..., default=...).Without this, callers passing typed records (e.g.
Contact(...),Event(...)) into*_setwould hitTypeError: 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.pyand runningscripts/e2e_typed_client.pyin hero_logic — typed Opportunity dataclasses round-trip cleanly through opportunity.set / opportunity.get.🤖 Generated with Claude Code
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>`_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>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>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>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>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.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.