Skip to content

feat: add private notes note-taking agent#231

Closed
Ju-usc wants to merge 14 commits intoopenhome-dev:devfrom
Ju-usc:feat/private-notes-agent
Closed

feat: add private notes note-taking agent#231
Ju-usc wants to merge 14 commits intoopenhome-dev:devfrom
Ju-usc:feat/private-notes-agent

Conversation

@Ju-usc
Copy link
Copy Markdown
Contributor

@Ju-usc Ju-usc commented Mar 29, 2026

Summary

  • add a new private-notes voice-first note-taking agent under community/private-notes
  • implement a single-prompt tool loop for create/read/update/delete note flows with confirmation and safe JSON persistence
  • document the behavior, storage model, and validation steps in the ability README and SPEC

Validation

  • python3 -m py_compile abilities/community/private-notes/main.py
  • python3 abilities/validate_ability.py abilities/community/private-notes

Ju-usc and others added 13 commits February 22, 2026 21:36
Introduce a WebUI-compatible ability that relays coding tasks to a Codex webhook with confirmation/cancel flow and conversational summaries. Document setup, trigger words, and a minimal Codex-focused /run webhook contract for contributors.
Replace explicit register_capability boilerplate with the template register tag and remove raw open() usage so the ability matches updated validation requirements.
- Rename folder, class, log tag, and voice prompts to be agent-agnostic
- Add _refine_prompt to clean up STT transcription before sending to agent
- README shows Claude Code and Codex equally with unified webhook example
- End-to-end tested via OpenHome + ngrok + Claude Code webhook
@Ju-usc Ju-usc requested a review from a team as a code owner March 29, 2026 23:10
Copilot AI review requested due to automatic review settings March 29, 2026 23:10
@github-actions
Copy link
Copy Markdown
Contributor

✅ Community PR Path Check — Passed

All changed files are inside the community/ folder. Looks good!

@github-actions
Copy link
Copy Markdown
Contributor

🔀 Branch Merge Check

PR direction: feat/private-notes-agentdev

Passedfeat/private-notes-agentdev is a valid merge direction

@github-actions github-actions bot added the community-ability Community-contributed ability label Mar 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

✅ Ability Validation Passed

📋 Validating: community/coding-agent-runner
  ✅ All checks passed!

📋 Validating: community/private-notes
  ✅ All checks passed!

@github-actions
Copy link
Copy Markdown
Contributor

🔍 Lint Results

🔧 Auto-formatted

Some files were automatically cleaned and formatted with autoflake + autopep8 and committed.

  • Unused imports removed (autoflake)
  • Unused variables removed (autoflake)
  • PEP8 formatting applied (autopep8)

__init__.py — Empty as expected

Files linted: community/coding-agent-runner/main.py community/private-notes/main.py

✅ Flake8 — Passed

✅ All checks passed!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new voice-first, file-persisted private note-taking ability using an LLM-driven tool loop, plus introduces a separate “coding agent runner” webhook ability.

Changes:

  • Added private-notes ability with CRUD note tools, confirmation gates, and JSON persistence.
  • Added documentation for private-notes (README + SPEC).
  • Added a new coding-agent-runner ability (webhook-driven coding task execution) and docs.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
community/private-notes/main.py Implements tool-loop note CRUD + persistence for private notes.
community/private-notes/README.md Documents usage, UX, and storage model for private notes.
community/private-notes/SPEC.md Defines architecture, data model, tools, and safety rules for private notes.
community/private-notes/init.py Package marker for the new ability.
community/coding-agent-runner/main.py Adds a webhook-based coding task execution ability.
community/coding-agent-runner/README.md Documents setup and webhook contract for coding agent runner.
community/coding-agent-runner/init.py Package marker for the new ability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +158 to +167
notes = sorted(
notebook["notes"], key=lambda n: n.get("updated_at", ""), reverse=True
)
note_index = {
"note_count": len(notes),
"notes": [
{"id": n.get("id"), "title": n.get("title"), "updated_at": n.get("updated_at")}
for n in notes
],
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

The note index included in the initial context is unbounded (it includes every note's id/title/updated_at). As the user accumulates notes, this can exceed the model context window and make the ability fail or behave unpredictably. Consider capping the index to a fixed number of most-recent notes (and/or adding a search/list tool to retrieve ids on demand).

Copilot uses AI. Check for mistakes.
notebook = await self._load_notebook()

# Capture time once so the context prefix stays identical across turns (LLM caching).
now = datetime.now(ZoneInfo(self.capability_worker.get_timezone()))
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

ZoneInfo(self.capability_worker.get_timezone()) can raise if the timezone string is missing/invalid. Right now that would abort the whole ability via the outer except. Consider wrapping this in a small try/except and falling back to UTC or naive datetime.now() so the ability can still function.

Suggested change
now = datetime.now(ZoneInfo(self.capability_worker.get_timezone()))
try:
tz = ZoneInfo(self.capability_worker.get_timezone())
except Exception:
tz = None
now = datetime.now(tz) if tz is not None else datetime.now()

Copilot uses AI. Check for mistakes.
if not existing:
return {"ok": False, "notes_changed": False, "error": "note not found"}

if not await self.capability_worker.run_confirmation_loop(args.get("confirmation", "")):
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

Overwrite confirmation is driven by args.get("confirmation", ""), which can become an empty string if the model omits/returns null for confirmation. That risks a confusing/blank confirmation prompt (or unexpected behavior inside run_confirmation_loop). Consider validating that a non-empty confirmation prompt was provided for overwrites, and returning a structured error (or forcing an ask_followup) if it's missing.

Suggested change
if not await self.capability_worker.run_confirmation_loop(args.get("confirmation", "")):
confirmation_prompt = args.get("confirmation")
if not isinstance(confirmation_prompt, str) or not confirmation_prompt.strip():
return {
"ok": False,
"notes_changed": False,
"error": "missing or empty confirmation prompt for overwrite",
}
if not await self.capability_worker.run_confirmation_loop(confirmation_prompt):

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +230

if not await self.capability_worker.run_confirmation_loop(args.get("confirmation", "")):
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

Delete confirmation is also taken from args.get("confirmation", ""), which can be empty/null if the model misbehaves. Since delete is destructive, consider requiring a non-empty confirmation prompt and returning an error/asking follow-up if it’s missing rather than calling run_confirmation_loop with an empty string.

Suggested change
if not await self.capability_worker.run_confirmation_loop(args.get("confirmation", "")):
confirmation = (args.get("confirmation") or "").strip()
# Require a non-empty confirmation prompt before running the confirmation loop.
if not confirmation:
return {
"ok": False,
"notes_changed": False,
"deleted_count": 0,
"status": "missing_confirmation",
}
if not await self.capability_worker.run_confirmation_loop(confirmation):

Copilot uses AI. Check for mistakes.
Comment on lines +140 to +145
## Validation

```
python3 -m py_compile abilities/community/private-notes/main.py
python3 abilities/validate_ability.py abilities/community/private-notes
```
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

The validation commands reference abilities/community/private-notes/..., but in this repo the path is community/private-notes/... (and validate_ability.py is at the repo root). This makes the instructions in the spec non-runnable as written; please update the paths to match the repository layout.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +12
"""OpenHome ability – voice-triggered coding task execution via webhook.

Flow: ask → confirm → refine prompt → call webhook → speak result.
"""

import asyncio

import requests
from src.agent.capability import MatchingCapability
from src.main import AgentWorker
from src.agent.capability_worker import CapabilityWorker

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

PR metadata describes only adding the private-notes ability, but this change set also introduces a full new coding-agent-runner ability. If this is intentional, it should be called out explicitly in the PR title/description; otherwise, consider splitting into a separate PR to keep review and release scope clear.

Copilot uses AI. Check for mistakes.
system_prompt=SYSTEM_PROMPT,
)
# LLMs sometimes wrap JSON in markdown fences
tool_call = json.loads(llm_response.replace("```json", "").replace("```", "").strip())
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

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

llm_response is parsed with json.loads(...) without handling JSONDecodeError. If the model returns non-JSON (or extra text), this will jump to the broad exception handler and abort the entire ability. Consider catching json.JSONDecodeError here, logging the raw response (careful about PII), and either retrying the LLM call or prompting the user/LLM with ask_followup to restate the request/tool call format.

Suggested change
tool_call = json.loads(llm_response.replace("```json", "").replace("```", "").strip())
sanitized_response = llm_response.replace("```json", "").replace("```", "").strip()
try:
tool_call = json.loads(sanitized_response)
except json.JSONDecodeError:
# Log the raw LLM response for debugging, being careful about PII.
self.worker.editor_logging_handler.warning(
"[PrivateNotes] Failed to parse LLM tool call JSON. Raw response (sanitized): %r",
sanitized_response,
)
await self.capability_worker.speak(
"I had trouble understanding that notes request. Could you say it again?"
)
followup = await self._get_user_input(
"I didn't catch anything, so I didn't change your notes."
)
if not followup:
return
history.append({"role": "user", "content": followup})
continue

Copilot uses AI. Check for mistakes.
@Ju-usc
Copy link
Copy Markdown
Contributor Author

Ju-usc commented Mar 29, 2026

Closing this PR because it was opened from the wrong branch and included unrelated coding-agent-runner history. It is superseded by #232, which contains only the private-notes changes based directly on dev.

@Ju-usc Ju-usc closed this Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-ability Community-contributed ability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants