Skip to content

Commit 51dfdcb

Browse files
refactor(commands): proxy rag.corpus-info through orchestration runtime (#24)
Phase D1 of the architecture-realignment spec, gui side. The ``rag.corpus-info`` executor body moved to attune-author in [attune-author#17]. This change shrinks the gui registration to a proxy CommandSpec whose executor dispatches via ``attune_author.orchestration.run_command(...)``. New helpers in commands.py: _proxy_command(name) mirror an orchestration spec into a gui CommandSpec. _orchestration_dispatcher(n) build a closure that converts the gui JobContext to the orchestration JobContext and unwraps RunResult.output. D2 (author.* commands) and D3 (help.* commands) will reuse both. Bumps gui to 0.6.0; pins attune-author[ai] to >=0.8.1,<0.9. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8c42458 commit 51dfdcb

4 files changed

Lines changed: 111 additions & 61 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,29 @@
33
All notable changes to `attune-gui` are documented here.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6+
## [0.6.0] — 2026-05-08
7+
8+
### Changed
9+
10+
- **`rag.corpus-info` migrated to `attune_author.orchestration`**
11+
Phase D1 of the architecture-realignment spec. The executor body
12+
now lives in attune-author; the gui keeps a thin `CommandSpec`
13+
whose executor dispatches through
14+
`attune_author.orchestration.run_command(...)`. `/api/commands`
15+
and the job runner are unchanged from the client's perspective.
16+
- New helpers in `attune_gui.commands`: `_proxy_command(name)`
17+
mirrors an attune-author orchestration spec into a gui
18+
`CommandSpec`; `_orchestration_dispatcher(name)` builds the
19+
closure that converts gui `JobContext` → orchestration
20+
`JobContext` and unwraps `RunResult.output`. Phases D2 / D3 will
21+
use the same helpers for the `author.*` and `help.*` migrations.
22+
23+
### Dependencies
24+
25+
- Bumped `attune-author[ai]` constraint from `>=0.5.0,<0.6` to
26+
`>=0.8.1,<0.9` (orchestration scaffold + the moved
27+
`rag.corpus-info` command).
28+
629
## [0.5.4] — 2026-05-06
730

831
### Security

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "attune-gui"
7-
version = "0.5.4"
7+
version = "0.6.0"
88
description = "Local dashboard for attune-rag / attune-help / attune-author. Server-rendered Jinja2 UI — ships clean via PyPI with no npm step."
99
readme = "README.md"
1010
requires-python = ">=3.10"
@@ -30,7 +30,7 @@ dependencies = [
3030
"pydantic>=2.0,<3.0",
3131
"structlog>=24.0,<26.0",
3232
"attune-rag>=0.1.12,<0.2",
33-
"attune-author[ai]>=0.5.0,<0.6",
33+
"attune-author[ai]>=0.8.1,<0.9",
3434
"attune-help>=0.10.0,<1.0",
3535
"jinja2>=3.1,<4.0",
3636
"python-frontmatter>=1.1,<2.0",

sidecar/attune_gui/commands.py

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,59 @@
2828
logger = logging.getLogger(__name__)
2929

3030

31+
def _orchestration_dispatcher(orchestration_name: str) -> ExecutorFn:
32+
"""Build an executor that hands the call off to attune-author's orchestration runtime.
33+
34+
The gui keeps a thin :class:`CommandSpec` so its profile filtering,
35+
job runner, and ``GET /api/commands`` continue to work with no
36+
changes. The actual executor body lives in attune-author. The
37+
returned closure converts the gui's :class:`JobContext` into the
38+
orchestration-runtime equivalent and unwraps ``RunResult.output``
39+
so the existing job runner sees a plain dict.
40+
"""
41+
42+
async def _dispatch(args: dict[str, Any], ctx: JobContext) -> Any:
43+
from attune_author.orchestration import ( # noqa: PLC0415
44+
JobContext as AuthorCtx,
45+
)
46+
from attune_author.orchestration import (
47+
run_command,
48+
)
49+
50+
author_ctx = AuthorCtx(job_id=ctx.job_id, log=ctx.log)
51+
result = await run_command(orchestration_name, args, author_ctx)
52+
return result.output
53+
54+
_dispatch.__name__ = f"_dispatch_{orchestration_name.replace('.', '_')}"
55+
return _dispatch
56+
57+
58+
def _proxy_command(orchestration_name: str) -> CommandSpec:
59+
"""Mirror an attune-author orchestration spec into a gui ``CommandSpec``.
60+
61+
Importing :mod:`attune_author.orchestration.commands.<ns>` triggers
62+
registration on the orchestration registry; this helper then copies
63+
the metadata into a gui-shaped ``CommandSpec`` whose executor is a
64+
dispatcher closure. Phases D2 and D3 will repeat this pattern for
65+
each migrated command, eventually leaving this module with only
66+
proxy registrations and the executors removed.
67+
"""
68+
69+
from attune_author.orchestration import COMMANDS as _AUTHOR_COMMANDS # noqa: PLC0415
70+
71+
src = _AUTHOR_COMMANDS[orchestration_name]
72+
return CommandSpec(
73+
name=src.name,
74+
title=src.title,
75+
domain=src.domain,
76+
description=src.description,
77+
args_schema=src.args_schema,
78+
executor=_orchestration_dispatcher(orchestration_name),
79+
cancellable=src.cancellable,
80+
profiles=src.profiles,
81+
)
82+
83+
3184
def _require_absolute(field: str, raw: str) -> None:
3285
"""Reject relative paths up-front so users see a clear error.
3386
@@ -327,54 +380,15 @@ async def _exec_author_generate(args: dict[str, Any], ctx: JobContext) -> dict[s
327380

328381

329382
# ---------------------------------------------------------------------------
330-
# RAG: corpus-info
383+
# RAG: corpus-info — owned by attune-author since Phase D1 of the
384+
# architecture-realignment spec. Importing the orchestration command
385+
# module triggers registration; we mirror it into this gui registry
386+
# so /api/commands and the job runner stay unchanged.
331387
# ---------------------------------------------------------------------------
332388

389+
import attune_author.orchestration.commands.rag # noqa: F401, E402
333390

334-
async def _exec_rag_corpus_info(args: dict[str, Any], ctx: JobContext) -> dict[str, Any]:
335-
from attune_gui.routes.rag import _get_pipeline # noqa: PLC0415
336-
from attune_gui.workspace import get_workspace # noqa: PLC0415
337-
338-
project_path_raw = str(args.get("project_path", "")).strip()
339-
workspace = (
340-
Path(project_path_raw).expanduser().resolve() if project_path_raw else get_workspace()
341-
)
342-
pipeline = _get_pipeline(workspace)
343-
entries = await asyncio.to_thread(list, pipeline.corpus.entries())
344-
kinds = sorted({e.path.split("/")[0] for e in entries if "/" in e.path})
345-
ctx.log(f"{len(entries)} entries across {len(kinds)} kind(s)")
346-
return {
347-
"corpus_class": type(pipeline.corpus).__name__,
348-
"entry_count": len(entries),
349-
"kinds": kinds,
350-
}
351-
352-
353-
COMMANDS["rag.corpus-info"] = CommandSpec(
354-
name="rag.corpus-info",
355-
title="Corpus info",
356-
domain="rag",
357-
description="Show entry count and category breakdown for the loaded attune-rag corpus.",
358-
args_schema={
359-
"type": "object",
360-
"properties": {
361-
"project_path": {
362-
"type": "string",
363-
"title": "Project path",
364-
"default": "",
365-
"description": (
366-
"Root of the project whose corpus to inspect. "
367-
"Leave blank to use the configured workspace."
368-
),
369-
"ui:widget": "path",
370-
"ui:browseHint": "project",
371-
},
372-
},
373-
},
374-
executor=_exec_rag_corpus_info,
375-
cancellable=False,
376-
profiles=("developer", "author"),
377-
)
391+
COMMANDS["rag.corpus-info"] = _proxy_command("rag.corpus-info")
378392

379393

380394
# ---------------------------------------------------------------------------

sidecar/tests/test_commands.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
_exec_help_list,
2626
_exec_help_lookup,
2727
_exec_help_search,
28-
_exec_rag_corpus_info,
2928
_exec_rag_query,
3029
_help_engine,
3130
_require_absolute,
@@ -250,20 +249,34 @@ async def test_query_returns_hits_and_augmented_prompt(
250249
assert out["augmented_prompt"] == "prompt body"
251250

252251
@pytest.mark.asyncio
253-
async def test_corpus_info_summarizes_kinds(self, ctx: FakeJobContext, tmp_path: Path) -> None:
254-
entries = [
255-
SimpleNamespace(path="security/concept.md"),
256-
SimpleNamespace(path="security/task.md"),
257-
SimpleNamespace(path="memory/concept.md"),
258-
]
259-
pipeline = MagicMock()
260-
pipeline.corpus.entries.return_value = iter(entries)
261-
with patch("attune_gui.routes.rag._get_pipeline", return_value=pipeline):
262-
out = await _exec_rag_corpus_info(
263-
{"project_path": str(tmp_path)}, ctx # type: ignore[arg-type]
252+
async def test_corpus_info_dispatches_to_attune_author(
253+
self, ctx: FakeJobContext, tmp_path: Path
254+
) -> None:
255+
# Phase D1: rag.corpus-info now lives in attune_author.orchestration.
256+
# The gui keeps a thin proxy CommandSpec; this test verifies the
257+
# dispatcher converts the gui ctx, calls run_command, and returns
258+
# the unwrapped output dict so the job runner sees a plain payload.
259+
from attune_author.orchestration import RunResult
260+
261+
spec = get_command("rag.corpus-info")
262+
assert spec is not None
263+
264+
async def fake_run_command(name, args, author_ctx): # noqa: ARG001
265+
author_ctx.log("dispatched")
266+
return RunResult(
267+
success=True,
268+
output={"entry_count": 3, "kinds": ["memory", "security"]},
269+
elapsed_ms=1,
264270
)
265-
assert out["entry_count"] == 3
266-
assert out["kinds"] == ["memory", "security"]
271+
272+
with patch("attune_author.orchestration.run_command", side_effect=fake_run_command):
273+
out = await spec.executor(
274+
{"project_path": str(tmp_path)},
275+
ctx, # type: ignore[arg-type]
276+
)
277+
278+
assert out == {"entry_count": 3, "kinds": ["memory", "security"]}
279+
assert "dispatched" in ctx.lines
267280

268281

269282
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)