import logging
import uuid
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Union
from briefcase.integrations.frameworks._export_mixin import ExportMixin
logger = logging.getLogger(__name__)
try:
from pageindex import PageIndexClient as _PageIndexClient
_PAGEINDEX_AVAILABLE = True
except ImportError:
_PageIndexClient = None _PAGEINDEX_AVAILABLE = False
_INSTALL_HINT = (
"pageindex is required for this operation. "
"Install with: pip install pageindex or pip install briefcase-ai[pageindex]"
)
def _compute_tree_depth(node: Dict[str, Any], current_depth: int = 0) -> int:
children = node.get("nodes", [])
if not children:
return current_depth
return max(_compute_tree_depth(child, current_depth + 1) for child in children)
def _count_tree_nodes(node: Dict[str, Any]) -> int:
children = node.get("nodes", [])
return 1 + sum(_count_tree_nodes(child) for child in children)
def _build_tree_path(node: Dict[str, Any], max_sections: int = 3) -> str:
parts: List[str] = []
root_title = node.get("title") or node.get("node_id") or "root"
parts.append(str(root_title))
children = node.get("nodes", [])
shown = children[:max_sections]
for child in shown:
child_title = child.get("title") or child.get("node_id") or "node"
parts.append(str(child_title))
if len(children) > max_sections:
parts.append(f"... ({len(children) - max_sections} more)")
return " > ".join(parts)
class PageIndexTracer(ExportMixin):
def __init__(
self,
api_key: Optional[str] = None,
context_version: Optional[str] = None,
async_capture: bool = True,
client: Optional[Any] = None,
fetch_tree_metadata: bool = True,
exporter: Any = None,
) -> None:
self.api_key = api_key
self.context_version = context_version
self.async_capture = async_capture
self.fetch_tree_metadata = fetch_tree_metadata
self._exporter = exporter
if client is not None:
self._client: Optional[Any] = client
elif _PAGEINDEX_AVAILABLE and api_key is not None:
self._client = _PageIndexClient(api_key=api_key)
else:
self._client = None
self._records: List[Dict[str, Any]] = []
def get_records(self) -> List[Dict[str, Any]]:
return list(self._records)
def clear(self) -> None:
self._records.clear()
def chat_completions(
self,
messages: List[Dict[str, str]],
doc_id: Optional[Union[str, List[str]]] = None,
**kwargs: Any,
) -> Any:
if self._client is None:
raise ImportError(_INSTALL_HINT)
started_at = datetime.now(timezone.utc)
decision_id = str(uuid.uuid4())
primary_doc_id = _normalize_doc_id(doc_id)
try:
response = self._client.chat_completions(
messages=messages, doc_id=doc_id, **kwargs
)
except Exception as exc:
ended_at = datetime.now(timezone.utc)
record = self._build_record(
decision_id=decision_id,
doc_id=primary_doc_id,
messages=messages,
response=None,
started_at=started_at,
ended_at=ended_at,
error=str(exc),
tree_meta=None,
)
self._records.append(record)
self._trigger_export(record)
raise
ended_at = datetime.now(timezone.utc)
tree_meta = self._fetch_tree_meta(primary_doc_id)
record = self._build_record(
decision_id=decision_id,
doc_id=primary_doc_id,
messages=messages,
response=response,
started_at=started_at,
ended_at=ended_at,
tree_meta=tree_meta,
)
self._records.append(record)
self._trigger_export(record)
return response
def get_tree(self, doc_id: str, **kwargs: Any) -> Any:
if self._client is None:
raise ImportError(_INSTALL_HINT)
return self._client.get_tree(doc_id, **kwargs)
def _fetch_tree_meta(self, doc_id: str) -> Dict[str, Any]:
if not self.fetch_tree_metadata or not doc_id or self._client is None:
return {}
try:
tree_response = self._client.get_tree(doc_id)
tree = tree_response.get("tree") or tree_response.get("data")
if not isinstance(tree, dict):
return {}
return {
"pageindex.tree.depth": _compute_tree_depth(tree),
"pageindex.tree.nodes_visited": _count_tree_nodes(tree),
"pageindex.tree.path": _build_tree_path(tree),
"pageindex.tree.backtrack_count": 0,
}
except Exception:
return {}
def _build_record(
self,
decision_id: str,
doc_id: str,
messages: List[Dict[str, str]],
response: Any,
started_at: datetime,
ended_at: datetime,
error: Optional[str] = None,
tree_meta: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
delta_ms = (ended_at - started_at).total_seconds() * 1000
record: Dict[str, Any] = {
"decision_id": decision_id,
"decision_type": "pageindex_retrieval",
"function_name": "PageIndexTracer.chat_completions",
"inputs": {"messages": messages, "doc_id": doc_id},
"outputs": {},
"started_at": started_at.isoformat(),
"ended_at": ended_at.isoformat(),
"execution_time_ms": delta_ms,
"pageindex.doc_id": doc_id,
"pageindex.retrieval_method": "tree_search",
}
if self.context_version is not None:
record["context_version"] = self.context_version
if error:
record["error"] = error
if response is not None and isinstance(response, dict):
choices = response.get("choices", [])
if choices:
content = choices[0].get("message", {}).get("content", "")
record["outputs"] = {"content": content}
if tree_meta:
record.update(tree_meta)
else:
record.setdefault("pageindex.tree.depth", 0)
record.setdefault("pageindex.tree.nodes_visited", 0)
record.setdefault("pageindex.tree.path", "")
record.setdefault("pageindex.tree.backtrack_count", 0)
return record
def _normalize_doc_id(doc_id: Optional[Union[str, List[str]]]) -> str:
if isinstance(doc_id, list):
return doc_id[0] if doc_id else ""
return str(doc_id) if doc_id is not None else ""
def require_pageindex() -> None:
if not _PAGEINDEX_AVAILABLE:
raise ImportError(_INSTALL_HINT)