honcho/honcho-cli/src/honcho_cli/commands/peer.py

309 lines
12 KiB
Python

"""Peer commands: list, inspect, card, chat, search, create, metadata, representation."""
from __future__ import annotations
import json
from typing import Optional
import typer
from honcho.api_types import PeerConfig
from honcho_cli.commands.workspace import _config_to_dict, _handle_error, _raw_list
from honcho_cli.output import print_error, print_result, use_json
from honcho_cli.validation import validate_resource_id
from honcho_cli._help import HonchoTyperGroup
from honcho_cli.common import add_common_options, get_client, get_resolved_config, handle_cmd_flags
app = typer.Typer(cls=HonchoTyperGroup, help="List, create, chat with, search, and manage peers and their representations.")
add_common_options(app)
def _get_peer_id(peer_id: str | None) -> str:
config = get_resolved_config()
pid = peer_id or config.peer_id
if not pid:
if not config.workspace_id:
print_error(
"NO_SCOPE",
"No peer or workspace scoped. Pass --peer/-p and --workspace/-w, or set HONCHO_PEER_ID and HONCHO_WORKSPACE_ID.",
)
else:
print_error("NO_PEER", "No peer ID provided. Pass --peer/-p or set HONCHO_PEER_ID.")
raise typer.Exit(1)
return validate_resource_id(pid, "peer")
@app.command("list")
def list_peers(
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""List all peers in the workspace."""
handle_cmd_flags(json_output=json_output, workspace=workspace)
client, config = get_client()
try:
raw_peers = _raw_list(client.peers())
items = [
{
"id": p.id,
"metadata": p.metadata,
"configuration": _config_to_dict(p.configuration) if p.configuration else None,
"created_at": str(p.created_at),
}
for p in raw_peers
]
print_result(items, columns=["id", "metadata", "created_at"], title="Peers")
except Exception as e:
_handle_error(e, "peer", "list")
@app.command()
def inspect(
peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Inspect a peer: card, session count, recent conclusions."""
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
pid = _get_peer_id(peer_id)
client, config = get_client()
p = client.peer(pid)
try:
card = p.get_card()
peer_config = p.get_configuration()
# First page only; SyncPage.total (when the server supplies it) is
# authoritative for counts without walking every page.
session_page = p.sessions()
conclusion_page = p.conclusions.list(size=10)
session_items = session_page.items
conclusion_items = conclusion_page.items
result = {
"id": pid,
"card": card,
"configuration": _config_to_dict(peer_config) if peer_config else None,
"session_count": session_page.total,
"conclusion_count": conclusion_page.total,
"recent_conclusions": [
{"id": c.id, "content": c.content if use_json() else c.content[:200], "created_at": str(c.created_at)}
for c in conclusion_items
],
"sessions": [{"id": s.id} for s in session_items[:10]],
}
print_result(result)
except Exception as e:
_handle_error(e, "peer", pid)
@app.command()
def card(
peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"),
target: Optional[str] = typer.Option(None, help="Target peer for relationship card"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Get raw peer card content."""
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
pid = _get_peer_id(peer_id)
client, config = get_client()
p = client.peer(pid)
try:
result = p.get_card(target=target)
print_result({"peer_id": pid, "target": target, "card": result})
except Exception as e:
_handle_error(e, "peer", pid)
@app.command()
def chat(
query: str = typer.Argument(help="Question to ask about the peer"),
target: Optional[str] = typer.Option(None, help="Target peer for perspective"),
reasoning: Optional[str] = typer.Option(None, "--reasoning", "-r", help="Reasoning level: minimal, low, medium, high, max"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Query the dialectic about a peer."""
_REASONING_LEVELS = ("minimal", "low", "medium", "high", "max")
if reasoning and reasoning not in _REASONING_LEVELS:
from honcho_cli.output import print_error
print_error("INVALID_REASONING", f"--reasoning must be one of: {', '.join(_REASONING_LEVELS)}")
raise typer.Exit(1)
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer, session=session)
pid = _get_peer_id(None)
client, config = get_client()
p = client.peer(pid)
try:
response = p.chat(
query,
target=target,
session=config.session_id or None,
reasoning_level=reasoning or None,
)
print_result({"peer_id": pid, "query": query, "response": response})
except Exception as e:
_handle_error(e, "peer", pid)
@app.command()
def search(
query: str = typer.Argument(help="Search query"),
limit: int = typer.Option(10, help="Max results"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Search a peer's messages."""
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
pid = _get_peer_id(None)
client, config = get_client()
p = client.peer(pid)
try:
results = p.search(query, limit=limit)
items = [
{
"id": m.id,
"content": m.content if use_json() else m.content[:200],
"session_id": m.session_id,
"created_at": str(m.created_at),
}
for m in results
]
print_result(items, columns=["id", "session_id", "content", "created_at"], title=f"Peer search: {query}")
except Exception as e:
_handle_error(e, "peer", pid)
@app.command("create")
def create_peer(
peer_id: str = typer.Argument(help="Peer ID to create or get"),
observe_me: Optional[bool] = typer.Option(None, "--observe-me/--no-observe-me", help="Whether Honcho will form a representation of this peer"),
metadata: Optional[str] = typer.Option(None, "--metadata", help="JSON metadata to associate with the peer"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Create or get a peer."""
handle_cmd_flags(json_output=json_output, workspace=workspace)
pid = validate_resource_id(peer_id, "peer")
client, config = get_client()
parsed_metadata = None
if metadata:
try:
parsed_metadata = json.loads(metadata)
except json.JSONDecodeError as e:
print_error("INVALID_JSON", f"--metadata must be valid JSON: {e}", {})
raise typer.Exit(1)
peer_config = PeerConfig(observe_me=observe_me) if observe_me is not None else None
try:
p = client.peer(pid, configuration=peer_config, metadata=parsed_metadata)
# Only round-trip to the server when the caller passed config or
# metadata — in that case get-or-create may have returned a
# pre-existing peer and the echoed output would lie. When no input
# was passed, skip the two extra API calls entirely.
result: dict[str, object] = {"peer_id": p.id}
if peer_config is not None or parsed_metadata is not None:
result["metadata"] = p.get_metadata()
result["configuration"] = _config_to_dict(p.get_configuration())
print_result(result)
except Exception as e:
_handle_error(e, "peer", pid)
@app.command("get-metadata")
def get_metadata(
peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Get metadata for a peer."""
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
pid = _get_peer_id(peer_id)
client, config = get_client()
p = client.peer(pid)
try:
result = p.get_metadata()
print_result({"peer_id": pid, "metadata": result})
except Exception as e:
_handle_error(e, "peer", pid)
@app.command("set-metadata")
def set_metadata(
metadata: str = typer.Argument(help="JSON metadata to set (e.g. '{\"key\": \"value\"}')"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Peer ID (uses default if omitted)"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Set metadata for a peer."""
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer)
pid = _get_peer_id(None)
client, config = get_client()
try:
parsed = json.loads(metadata)
except json.JSONDecodeError as e:
print_error("INVALID_JSON", f"metadata must be valid JSON: {e}", {})
raise typer.Exit(1)
p = client.peer(pid)
try:
p.set_metadata(parsed)
print_result({"peer_id": pid, "metadata": parsed})
except Exception as e:
_handle_error(e, "peer", pid)
@app.command()
def representation(
peer_id: Optional[str] = typer.Argument(None, help="Peer ID (uses default if omitted)"),
target: Optional[str] = typer.Option(None, help="Target peer to get representation about"),
search_query: Optional[str] = typer.Option(None, help="Semantic search query to filter conclusions"),
max_conclusions: Optional[int] = typer.Option(None, help="Maximum number of conclusions to include"),
workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Override workspace ID"),
peer: Optional[str] = typer.Option(None, "--peer", "-p", help="Override peer ID"),
session: Optional[str] = typer.Option(None, "--session", "-s", help="Override session ID"),
json_output: bool = typer.Option(False, "--json", help="Force JSON output"),
) -> None:
"""Get the formatted representation for a peer."""
handle_cmd_flags(json_output=json_output, workspace=workspace, peer=peer, session=session)
pid = _get_peer_id(peer_id)
client, config = get_client()
p = client.peer(pid)
try:
result = p.representation(
target=target,
session=config.session_id or None,
search_query=search_query,
max_conclusions=max_conclusions,
)
print_result({"peer_id": pid, "target": target, "representation": result})
except Exception as e:
_handle_error(e, "peer", pid)