feat: unify card runtime and event-driven web ui
This commit is contained in:
parent
0edf8c3fef
commit
4dfb7ca3cc
105 changed files with 17382 additions and 8505 deletions
157
inbox_service.py
Normal file
157
inbox_service.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from collections.abc import Awaitable, Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
NotificationEventFactory = Callable[[dict[str, Any]], dict[str, Any] | None]
|
||||
RunNanobotTurn = Callable[..., Awaitable[dict[str, Any]]]
|
||||
|
||||
|
||||
class InboxService:
|
||||
def __init__(self, *, repo_dir: Path, inbox_dir: Path) -> None:
|
||||
self._repo_dir = repo_dir
|
||||
self._inbox_dir = inbox_dir
|
||||
self._module: Any | None = None
|
||||
|
||||
def _load_module(self):
|
||||
if self._module is not None:
|
||||
return self._module
|
||||
|
||||
script_path = self._repo_dir / "scripts" / "inbox_board.py"
|
||||
if not script_path.exists():
|
||||
raise RuntimeError(f"missing helper script: {script_path}")
|
||||
spec = importlib.util.spec_from_file_location("nanobot_web_inbox_board", script_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"failed to load helper script: {script_path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules.setdefault("nanobot_web_inbox_board", module)
|
||||
spec.loader.exec_module(module)
|
||||
module.ensure_inbox(self._inbox_dir)
|
||||
self._module = module
|
||||
return module
|
||||
|
||||
def list_items(
|
||||
self,
|
||||
*,
|
||||
status_filter: str | None,
|
||||
kind_filter: str | None,
|
||||
include_closed: bool,
|
||||
limit: int | None,
|
||||
tags: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
inbox_board = self._load_module()
|
||||
items = inbox_board.filter_items(
|
||||
inbox_board.collect_items(self._inbox_dir),
|
||||
status=status_filter,
|
||||
kind=kind_filter,
|
||||
tags=tags,
|
||||
include_closed=include_closed,
|
||||
)
|
||||
if limit is not None:
|
||||
items = items[:limit]
|
||||
return [item.to_dict() for item in items]
|
||||
|
||||
def open_items(self, limit: int = 4) -> list[dict[str, Any]]:
|
||||
return self.list_items(
|
||||
status_filter=None,
|
||||
kind_filter=None,
|
||||
include_closed=False,
|
||||
limit=limit,
|
||||
tags=[],
|
||||
)
|
||||
|
||||
def capture_item(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
text: str,
|
||||
kind: str,
|
||||
source: str,
|
||||
due: str,
|
||||
body: str,
|
||||
session_id: str,
|
||||
tags: list[str],
|
||||
confidence: int | float | None,
|
||||
) -> dict[str, Any]:
|
||||
inbox_board = self._load_module()
|
||||
created_path = inbox_board.create_item(
|
||||
self._inbox_dir,
|
||||
title=title.strip(),
|
||||
kind=kind,
|
||||
source=source,
|
||||
confidence=confidence,
|
||||
suggested_due=due.strip(),
|
||||
tags=tags,
|
||||
body=body.strip(),
|
||||
raw_text=text.strip(),
|
||||
metadata={"source_session": session_id.strip()} if session_id.strip() else None,
|
||||
)
|
||||
return inbox_board.parse_item(created_path).to_dict()
|
||||
|
||||
def dismiss_item(self, item: str) -> dict[str, Any]:
|
||||
inbox_board = self._load_module()
|
||||
updated_path = inbox_board.dismiss_item(self._inbox_dir, item)
|
||||
return inbox_board.parse_item(updated_path).to_dict()
|
||||
|
||||
async def accept_item_as_task(
|
||||
self,
|
||||
*,
|
||||
item: str,
|
||||
lane: str,
|
||||
title: str,
|
||||
due: str,
|
||||
body_text: str | None,
|
||||
tags: list[str] | None,
|
||||
run_nanobot_turn: RunNanobotTurn,
|
||||
notification_to_event: NotificationEventFactory,
|
||||
) -> dict[str, Any]:
|
||||
inbox_board = self._load_module()
|
||||
parsed_item = inbox_board.parse_item(inbox_board.resolve_item_path(self._inbox_dir, item))
|
||||
item_path = str(parsed_item.path)
|
||||
|
||||
instruction_lines = [
|
||||
"This is a UI inbox action. Groom the inbox item into a task if it is actionable.",
|
||||
f"Inbox item path: {item_path}",
|
||||
"Use the inbox_board tool and the task_helper_card tool.",
|
||||
"First inspect the item with action=show.",
|
||||
"Then improve the title, description, tags, or due date only if the capture supports it.",
|
||||
"If it is actionable, accept it into the task board.",
|
||||
"After accepting it, decide whether the task would be easier to execute immediately with a linked helper card.",
|
||||
"Create a helper card whenever it would clearly reduce friction for the user instead of making them search or prepare things manually.",
|
||||
"Use these patterns by default when they fit:",
|
||||
"- watch/learn -> create a watch helper card",
|
||||
"- read/research -> create a reading/reference helper card",
|
||||
"- go somewhere -> create a travel/map helper card",
|
||||
"- buy/order -> create a shopping/product helper card",
|
||||
"- call/email/reach out -> create an outreach draft helper card",
|
||||
"If you have enough information to search or enrich the helper card, do that first.",
|
||||
"If not, still create the fallback helper card from the task so the user has something actionable in the feed.",
|
||||
"Prefer backlog unless it is clearly committed or in-progress work.",
|
||||
"Do not invent details and do not ask follow-up questions.",
|
||||
"Reply with a short confirmation of what you did, including whether you created a helper card.",
|
||||
]
|
||||
if title.strip():
|
||||
instruction_lines.append(f"Title hint from UI: {title.strip()}")
|
||||
if body_text is not None and body_text.rstrip():
|
||||
instruction_lines.append(f"Description hint from UI: {body_text.rstrip()}")
|
||||
if due.strip():
|
||||
instruction_lines.append(f"Due hint from UI: {due.strip()}")
|
||||
if tags:
|
||||
instruction_lines.append(f"Tag hint from UI: {', '.join(str(tag) for tag in tags)}")
|
||||
if lane and lane != "backlog":
|
||||
instruction_lines.append(f"Preferred destination lane: {lane}")
|
||||
|
||||
return await run_nanobot_turn(
|
||||
"\n".join(instruction_lines),
|
||||
chat_id="inbox-groomer",
|
||||
metadata={
|
||||
"source": "web-inbox-card",
|
||||
"ui_action": "accept_task",
|
||||
"inbox_item": item_path,
|
||||
},
|
||||
timeout_seconds=90.0,
|
||||
notification_to_event=notification_to_event,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue