oc_chat
Loading...
Searching...
No Matches
tool_prompt.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/tool_prompt.py
4
5"""
6Compact tool-map renderer for the system prompt — plan 135 (§3.3).
7
8Purpose
9-------
10Render a one-line-per-tool routing table from the live (mode-filtered) tool
11schemas, replacing the ~21 KB embedded JSON-Schema dump + the hand-maintained
12quick-reference card in ``messages[0]``. The authoritative, machine-readable
13JSON Schema for every tool still travels via the provider's API ``tools``
14parameter; this map is only the human/LLM-readable routing aid.
15
16Responsibilities
17----------------
18- :func:`compact_tool_map` — pure function: schemas → Markdown table (one row
19 per tool, sorted, deterministic), ``*`` marking required params, ``[confirm]``
20 marking confirmation-gated tools, Markdown pipes escaped, ending with a stable
21 sentinel so re-attach logic can detect presence.
22
23Diagnostics
24-----------
25Domain: OC_CHAT.PROMPT
26Levels:
27 L2 — lifecycle events
28 L3 — semantic details
29 L4 — deep tracing
30
31Contracts
32---------
33- Pure: no I/O, no global state; deterministic for a given schema list.
34- Empty ``schemas`` → ``""`` (never raises).
35- Tolerates the OpenAI ``{"type":"function","function":{…}}`` wrapper and the
36 flat ``{"name":…,"parameters":…}`` shape.
37- The compact-map sentinel is the LAST line of the returned block.
38
39Dependencies
40------------
41- oc_diagnostics (structured debug logging via _dbg)
42"""
43from __future__ import annotations
44
45import re
46from typing import Any
47
48from oc_diagnostics import _dbg
49
50_dbg("OC_CHAT.PROMPT", "tool_prompt", "module", 4)
51
52#: Stable presence marker; ``ensure_system_includes_tool_reference`` keys on it.
53COMPACT_TOOL_MAP_SENTINEL = "<!--oc:compact-tool-map:v1-->"
54
55#: Tools whose confirmation gate is not expressed as a ``confirmed`` parameter.
56#: Add a name here only if a gated tool lacks the ``confirmed`` arg (§3.3); do
57#: not silently drop the marker. Empty today (``run_command`` self-declares).
58_GATED_TOOLS: frozenset[str] = frozenset()
59
60_PIPE_RE = re.compile(r"(?<!\\)\|") # unescaped pipe only — never double-escape
61
62
63def _one_line(text: str, limit: int) -> str:
64 """First sentence of *text*, whitespace-collapsed, capped at *limit* chars."""
65 text = " ".join((text or "").split())
66 if not text:
67 return ""
68 for end in (". ", "? ", "! "):
69 i = text.find(end)
70 if 0 < i <= limit:
71 return text[: i + 1].rstrip()
72 if len(text) <= limit:
73 return text
74 head = text[:limit]
75 if " " in head: # drop the partial trailing word — no mid-word break
76 head = head.rsplit(" ", 1)[0]
77 return head.rstrip() + " …"
78
79
80def _escape_cell(text: str) -> str:
81 """Collapse whitespace and escape unescaped Markdown table pipes (no double-escape)."""
82 return _PIPE_RE.sub(r"\|", " ".join((text or "").split()))
83
84
85def _params(fn: dict[str, Any]) -> str:
86 """``required*`` params first, then optional; ``-`` when the tool takes none."""
87 params = fn.get("parameters") or {}
88 props = params.get("properties") or {}
89 required = set(params.get("required") or [])
90 if not props:
91 return "-"
92 ordered = [p for p in props if p in required] + [p for p in props if p not in required]
93 return ", ".join(p + ("*" if p in required else "") for p in ordered)
94
95
96def _is_gated(name: str, fn: dict[str, Any]) -> bool:
97 """True when *fn* is confirmation-gated (``confirmed`` param or in the gated set)."""
98 props = (fn.get("parameters") or {}).get("properties") or {}
99 return "confirmed" in props or name in _GATED_TOOLS
100
101
103 schemas: list[dict[str, Any]], *, max_purpose: int = 160, confirm_marker: bool = True
104) -> str:
105 """Return a Markdown routing table for *schemas*, one row per tool.
106
107 Sorted by name; ``*`` marks a required param; ``[confirm]`` marks a
108 confirmation-gated tool. The block ends with
109 :data:`COMPACT_TOOL_MAP_SENTINEL`. Empty *schemas* → ``""``.
110 """
111 if not schemas:
112 return ""
113 rows: list[tuple[str, str, str, str]] = []
114 for sch in schemas:
115 fn = sch.get("function", sch)
116 name = str(fn.get("name", "?"))
117 marker = " [confirm]" if confirm_marker and _is_gated(name, fn) else ""
118 purpose = _escape_cell(_one_line(str(fn.get("description", "")), max_purpose))
119 params = _escape_cell(_params(fn))
120 rows.append((name, marker, purpose, params))
121 rows.sort(key=lambda r: r[0])
122 lines = ["| Tool | Purpose | Parameters |", "|------|---------|------------|"]
123 lines += [f"| `{name}`{marker} | {purpose} | {params} |" for name, marker, purpose, params in rows]
124 lines += [
125 "",
126 f"_{len(rows)} tools. Generated from the live registry — do not hand-edit._",
127 "",
128 COMPACT_TOOL_MAP_SENTINEL,
129 ]
130 return "\n".join(lines)
131
132
133__all__ = ["compact_tool_map", "COMPACT_TOOL_MAP_SENTINEL"]
str _one_line(str text, int limit)
bool _is_gated(str name, dict[str, Any] fn)
str _escape_cell(str text)
str _params(dict[str, Any] fn)
str compact_tool_map(list[dict[str, Any]] schemas, *, int max_purpose=160, bool confirm_marker=True)