Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,16 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres
explicit message.
- **The Cockpit Observe dashboard was redesigned.** Shared design tokens now live
in `agentops.agent.ui_theme`; Observe uses an explicit `data-theme` attribute
with an in-memory toggle instead of following the OS colour scheme, and the
Overview is organised around executive KPI cards. The Cockpit itself now renders
its tokens from the same module, so the two surfaces cannot drift.
instead of following the OS colour scheme, and the Overview is organised around
executive KPI cards. Cockpit and Observe now use the same dark/light control and
carry the non-sensitive theme preference in navigation URLs, without browser
storage or cookies.
- **Observe usage tables now present telemetry consistently.** Agents, models,
tools, runs, and drill-through details use compact UTC timestamps, seconds for
durations, separate cache-read, cache-write, and reasoning-token columns,
contextual information icons, and totals for additive metrics. Model deployment
falls back to the request model when a deployment attribute is absent, while the
response model remains independently visible.
- **Observability readiness only reports what it can actually verify.** Multi-turn
coverage is treated as a dataset property and inferred solely from conversation
rows; rubric evaluators count as ready only when they are both declared and
Expand Down
35 changes: 32 additions & 3 deletions src/agentops/agent/cockpit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
``agentops cockpit`` boots a tiny FastAPI server that reads the
analysis history from ``.agentops/agent/history.jsonl`` **and** the
evaluation history from ``.agentops/results/*/results.json``, then
serves a single cockpit page in a dark theme. No external frontend
serves a single cockpit page with explicit dark and light themes. No external frontend
dependencies (sparklines are inline SVG); no Azure resource required.

The server is intentionally read-only and bound to ``127.0.0.1`` by
Expand All @@ -28,7 +28,11 @@

from agentops.agent.history import AnalysisRecord, load_analysis_history
from agentops.agent.time_range import TimeRange
from agentops.agent.ui_theme import render_theme_variables
from agentops.agent.ui_theme import (
THEME_TOGGLE_SCRIPT,
render_theme_toggle,
render_theme_variables,
)
from agentops.core.attribution import load_attribution_config
from agentops.core.cost import MAX_COST_COMPONENTS, MAX_COST_PERIODS, load_cost_model
from agentops.core.governance import (
Expand Down Expand Up @@ -4463,6 +4467,10 @@ def render_cockpit_html(payload: Dict[str, Any]) -> str:
workspace_display=workspace_display,
workspace=payload["workspace"],
icon_uri=_icon_data_uri(),
theme_toggle=render_theme_toggle(
control_id="cockpit-theme-toggle", extra_class="cockpit-theme-toggle"
),
theme_script=THEME_TOGGLE_SCRIPT,
)


Expand Down Expand Up @@ -4517,6 +4525,22 @@ def _shorten_workspace(path: str) -> str:
display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
color: var(--text-dim); font-size: 12px; font-weight: 500;
}}
header .header-actions {{
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}}
.aos-btn {{
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 13px; border-radius: 8px;
border: 1px solid var(--border); background: var(--card);
color: var(--text); font: inherit; font-size: 13px; font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}}
.aos-btn:hover {{ border-color: var(--border-strong); background: var(--card-hi); }}
.aos-btn:focus-visible {{
outline: 2px solid var(--accent); outline-offset: 2px;
}}
.aos-theme-toggle .aos-theme-icon {{ font-size: 14px; line-height: 1; }}
header .stats-counts {{
display: flex; align-items: center; gap: 18px;
}}
Expand Down Expand Up @@ -5304,7 +5328,10 @@ def _shorten_workspace(path: str) -> str:
<div class="subtitle" title="{workspace}">{workspace_display}</div>
</div>
</div>
<a class="observe-link" href="/observe">Open Observe</a>
<div class="header-actions">
<a class="observe-link" href="/observe" data-theme-link>Open Observe</a>
{theme_toggle}
</div>
</header>

{status_cards_section}
Expand All @@ -5316,6 +5343,8 @@ def _shorten_workspace(path: str) -> str:
<footer><code>agentops cockpit</code></footer>

<script>
{theme_script}
setupAgentOpsThemeToggle();
// Auto-expand a collapsed <details> section when it is targeted via the
// URL hash (status cards and Next-actions CTAs link to #section-... ids).
// Details sections collapse by default now, so anchor navigation must open
Expand Down
37 changes: 28 additions & 9 deletions src/agentops/agent/observe/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ def _dimension_filters(
if filters.model:
value = _kql_escape(filters.model)
clauses.append(
'| where tostring(coalesce(Properties["gen_ai.request.model"], '
'Properties["gen_ai.response.model"])) '
'| where tostring(coalesce(Properties["gen_ai.response.model"], '
'Properties["gen_ai.request.model"])) '
f"== '{value}'"
)
return clauses
Expand All @@ -172,8 +172,8 @@ def _agent_extend_clauses() -> list[str]:
'| extend provider_name = tostring(Properties["gen_ai.provider.name"])',
'| extend system = tostring(Properties["gen_ai.system"])',
'| extend operation_name = tostring(Properties["gen_ai.operation.name"])',
'| extend model = tostring(coalesce(Properties["gen_ai.request.model"], '
'Properties["gen_ai.response.model"]))',
'| extend model = tostring(coalesce(Properties["gen_ai.response.model"], '
'Properties["gen_ai.request.model"]))',
'| extend input_tokens = toint(Properties["gen_ai.usage.input_tokens"])',
'| extend output_tokens = toint(Properties["gen_ai.usage.output_tokens"])',
]
Expand Down Expand Up @@ -268,6 +268,7 @@ def build_overview_query(
_time_window_clause(filters),
*_dimension_filters(filters, scope_source),
*_agent_extend_clauses(),
*_token_class_extend_clauses(),
'| where operation_name == "invoke_agent"',
'| extend is_request_invocation = TelemetryTable endswith "AppRequests", '
'is_dependency_invocation = TelemetryTable endswith "AppDependencies"',
Expand Down Expand Up @@ -303,6 +304,7 @@ def build_agents_query(
_time_window_clause(filters),
*_dimension_filters(filters, scope_source),
*_agent_extend_clauses(),
*_token_class_extend_clauses(),
'| extend is_request_invocation = TelemetryTable endswith "AppRequests" and '
'operation_name == "invoke_agent", '
'is_dependency_invocation = TelemetryTable endswith "AppDependencies" and '
Expand All @@ -317,6 +319,12 @@ def build_agents_query(
"iff(is_dependency_invocation, DurationMs, real(null)), 95), "
"input_tokens = sum(input_tokens), "
"output_tokens = sum(output_tokens), "
"cache_read_tokens = sum(cache_read_tokens), "
"cache_read_reporting_records = countif(isnotnull(cache_read_tokens)), "
"cache_write_tokens = sum(cache_write_tokens), "
"cache_write_reporting_records = countif(isnotnull(cache_write_tokens)), "
"reasoning_tokens = sum(reasoning_tokens), "
"reasoning_reporting_records = countif(isnotnull(reasoning_tokens)), "
"last_seen = max(TimeGenerated), "
"agent_id = take_anyif(agent_id, isnotempty(agent_id)), "
"agent_name = take_anyif(agent_name, isnotempty(agent_name)), "
Expand All @@ -329,9 +337,16 @@ def build_agents_query(
"failures = iff(request_invocations > 0, request_failures, dependency_failures), "
"p95_latency_ms = iff(request_invocations > 0, "
"request_p95_latency_ms, dependency_p95_latency_ms)",
"| extend cache_read_tokens = iff(cache_read_reporting_records > 0, "
"cache_read_tokens, long(null)), "
"cache_write_tokens = iff(cache_write_reporting_records > 0, "
"cache_write_tokens, long(null)), "
"reasoning_tokens = iff(reasoning_reporting_records > 0, "
"reasoning_tokens, long(null))",
"| project-away request_invocations, dependency_invocations, "
"request_failures, dependency_failures, request_p95_latency_ms, "
"dependency_p95_latency_ms",
"dependency_p95_latency_ms, cache_read_reporting_records, "
"cache_write_reporting_records, reasoning_reporting_records",
"| where invocations > 0",
]
return _bounded_aggregate(aggregate_lines, order_by="invocations")
Expand All @@ -346,7 +361,8 @@ def build_models_query(
_time_window_clause(filters),
*_dimension_filters(filters, scope_source),
*_agent_extend_clauses(),
'| extend deployment = tostring(Properties["gen_ai.request.deployment"])',
'| extend deployment = tostring(coalesce('
'Properties["gen_ai.request.deployment"], Properties["gen_ai.request.model"]))',
*_token_class_extend_clauses(),
"| where isnotempty(model) or isnotempty(deployment)",
'| where operation_name !in ("invoke_agent", "execute_tool")',
Expand Down Expand Up @@ -597,7 +613,8 @@ def build_drilldown_query(
_time_window_clause(filters),
*_dimension_filters(filters, scope_source),
*_agent_extend_clauses(),
'| extend deployment = tostring(Properties["gen_ai.request.deployment"]), '
'| extend deployment = tostring(coalesce('
'Properties["gen_ai.request.deployment"], Properties["gen_ai.request.model"])), '
'tool_name = tostring(Properties["gen_ai.tool.name"]), '
'conversation_id = tostring(Properties["gen_ai.conversation.id"]), '
'foundry_thread_id = tostring(Properties["gen_ai.thread.id"])',
Expand Down Expand Up @@ -768,7 +785,8 @@ def build_department_usage_query(
_time_window_clause(filters),
*_dimension_filters(filters, scope_source),
*_agent_extend_clauses(),
'| extend deployment = tostring(Properties["gen_ai.request.deployment"])',
'| extend deployment = tostring(coalesce('
'Properties["gen_ai.request.deployment"], Properties["gen_ai.request.model"]))',
'| extend tool_name = tostring(Properties["gen_ai.tool.name"]), '
'operation_name = tostring(Properties["gen_ai.operation.name"])',
'| extend conversation_id = tostring(Properties["gen_ai.conversation.id"]), '
Expand Down Expand Up @@ -979,7 +997,8 @@ def build_user_usage_query(
_time_window_clause(filters),
*_dimension_filters(filters, scope_source),
*_agent_extend_clauses(),
'| extend deployment = tostring(Properties["gen_ai.request.deployment"])',
'| extend deployment = tostring(coalesce('
'Properties["gen_ai.request.deployment"], Properties["gen_ai.request.model"]))',
'| extend tool_name = tostring(Properties["gen_ai.tool.name"]), '
'operation_name = tostring(Properties["gen_ai.operation.name"])',
'| extend conversation_id = tostring(Properties["gen_ai.conversation.id"]), '
Expand Down
3 changes: 3 additions & 0 deletions src/agentops/agent/observe/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ def normalize_agent_row(
p95_latency_ms=row.get("p95_latency_ms"),
input_tokens=row.get("input_tokens"),
output_tokens=row.get("output_tokens"),
cache_read_tokens=row.get("cache_read_tokens"),
cache_write_tokens=row.get("cache_write_tokens"),
reasoning_tokens=row.get("reasoning_tokens"),
)


Expand Down
Loading
Loading