foundry-mcp 0.8.22__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Overview page - dashboard home with KPIs and summary."""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
|
|
5
|
+
from foundry_mcp.dashboard.components.cards import kpi_row
|
|
6
|
+
from foundry_mcp.dashboard.data.stores import (
|
|
7
|
+
get_overview_summary,
|
|
8
|
+
get_errors,
|
|
9
|
+
get_error_patterns,
|
|
10
|
+
get_top_tool_actions,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render():
|
|
15
|
+
"""Render the Overview page."""
|
|
16
|
+
st.header("Overview")
|
|
17
|
+
|
|
18
|
+
# Get summary data
|
|
19
|
+
summary = get_overview_summary()
|
|
20
|
+
|
|
21
|
+
# KPI Cards Row
|
|
22
|
+
st.subheader("Key Metrics")
|
|
23
|
+
kpi_row(
|
|
24
|
+
[
|
|
25
|
+
{
|
|
26
|
+
"label": "Total Invocations",
|
|
27
|
+
"value": summary.get("total_invocations", 0),
|
|
28
|
+
"help": "Total tool invocations recorded (all time)",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"label": "Total Errors",
|
|
32
|
+
"value": summary.get("error_count", 0),
|
|
33
|
+
"help": "Total errors recorded (all time)",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
columns=2,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
st.divider()
|
|
40
|
+
|
|
41
|
+
# Bottom Row - Patterns and Recent
|
|
42
|
+
col1, col2 = st.columns(2)
|
|
43
|
+
|
|
44
|
+
with col1:
|
|
45
|
+
st.subheader("Top Error Patterns (All Time)")
|
|
46
|
+
patterns = get_error_patterns(min_count=2)
|
|
47
|
+
if patterns:
|
|
48
|
+
for i, p in enumerate(patterns[:5]):
|
|
49
|
+
with st.container(border=True):
|
|
50
|
+
st.markdown(f"**{p.get('tool_name', 'Unknown')}**")
|
|
51
|
+
st.caption(f"Count: {p.get('count', 0)} | Code: {p.get('error_code', 'N/A')}")
|
|
52
|
+
if p.get("message"):
|
|
53
|
+
st.text(p["message"][:100] + "..." if len(p.get("message", "")) > 100 else p.get("message", ""))
|
|
54
|
+
else:
|
|
55
|
+
st.info("No recurring error patterns detected")
|
|
56
|
+
|
|
57
|
+
with col2:
|
|
58
|
+
st.subheader("Recent Errors (Last Hour)")
|
|
59
|
+
errors_df = get_errors(since_hours=1, limit=5)
|
|
60
|
+
|
|
61
|
+
# Try fallback to 24h if 1h is empty
|
|
62
|
+
if errors_df is None or errors_df.empty:
|
|
63
|
+
errors_df = get_errors(since_hours=24, limit=5)
|
|
64
|
+
if errors_df is not None and not errors_df.empty:
|
|
65
|
+
st.caption("No errors in last hour - showing last 24h")
|
|
66
|
+
|
|
67
|
+
if errors_df is not None and not errors_df.empty:
|
|
68
|
+
for _, row in errors_df.iterrows():
|
|
69
|
+
with st.container(border=True):
|
|
70
|
+
st.markdown(f"**{row.get('tool_name', 'Unknown')}**")
|
|
71
|
+
st.caption(f"{row.get('timestamp', '')} | {row.get('error_code', 'N/A')}")
|
|
72
|
+
msg = row.get("message", "")
|
|
73
|
+
st.text(msg[:80] + "..." if len(msg) > 80 else msg)
|
|
74
|
+
else:
|
|
75
|
+
st.info("No recent errors")
|
|
76
|
+
|
|
77
|
+
st.divider()
|
|
78
|
+
|
|
79
|
+
# Tool Usage Breakdown
|
|
80
|
+
st.subheader("Top Tool Actions (Last 24h)")
|
|
81
|
+
top_actions = get_top_tool_actions(since_hours=24, top_n=10)
|
|
82
|
+
|
|
83
|
+
if top_actions:
|
|
84
|
+
col1, col2 = st.columns(2)
|
|
85
|
+
for i, item in enumerate(top_actions):
|
|
86
|
+
tool = item.get("tool", "unknown")
|
|
87
|
+
action = item.get("action", "")
|
|
88
|
+
count = int(item.get("count", 0))
|
|
89
|
+
display_name = f"{tool}.{action}" if action and action != "(no action)" else tool
|
|
90
|
+
target_col = col1 if i < 5 else col2
|
|
91
|
+
with target_col:
|
|
92
|
+
with st.container(border=True):
|
|
93
|
+
st.markdown(f"**{display_name}**")
|
|
94
|
+
st.caption(f"Invocations: {count:,}")
|
|
95
|
+
else:
|
|
96
|
+
st.info("No tool action data available yet")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Providers page - AI provider status grid."""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
|
|
5
|
+
from foundry_mcp.dashboard.components.cards import status_badge
|
|
6
|
+
from foundry_mcp.dashboard.data.stores import get_providers
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render():
|
|
10
|
+
"""Render the Providers page."""
|
|
11
|
+
st.header("AI Providers")
|
|
12
|
+
|
|
13
|
+
providers = get_providers()
|
|
14
|
+
|
|
15
|
+
if not providers:
|
|
16
|
+
st.info("No AI providers configured or providers module not available.")
|
|
17
|
+
st.caption("Providers are configured in foundry-mcp.toml under [providers]")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# Summary
|
|
21
|
+
available_count = len([p for p in providers if p.get("available")])
|
|
22
|
+
total_count = len(providers)
|
|
23
|
+
|
|
24
|
+
col1, col2, col3 = st.columns(3)
|
|
25
|
+
with col1:
|
|
26
|
+
st.metric("Total Providers", total_count)
|
|
27
|
+
with col2:
|
|
28
|
+
st.metric("Available", available_count)
|
|
29
|
+
with col3:
|
|
30
|
+
st.metric("Unavailable", total_count - available_count)
|
|
31
|
+
|
|
32
|
+
st.divider()
|
|
33
|
+
|
|
34
|
+
# Provider grid
|
|
35
|
+
st.subheader("Provider Status")
|
|
36
|
+
|
|
37
|
+
# Create grid layout
|
|
38
|
+
cols = st.columns(3)
|
|
39
|
+
|
|
40
|
+
for i, provider in enumerate(providers):
|
|
41
|
+
with cols[i % 3]:
|
|
42
|
+
with st.container(border=True):
|
|
43
|
+
# Header with status
|
|
44
|
+
provider_id = provider.get("id", "unknown")
|
|
45
|
+
is_available = provider.get("available", False)
|
|
46
|
+
|
|
47
|
+
status = "available" if is_available else "unavailable"
|
|
48
|
+
status_badge(status, label=provider_id)
|
|
49
|
+
|
|
50
|
+
# Description
|
|
51
|
+
description = provider.get("description", "")
|
|
52
|
+
if description:
|
|
53
|
+
st.caption(description[:100] + "..." if len(description) > 100 else description)
|
|
54
|
+
|
|
55
|
+
# Tags
|
|
56
|
+
tags = provider.get("tags", [])
|
|
57
|
+
if tags:
|
|
58
|
+
st.markdown(" ".join([f"`{tag}`" for tag in tags[:5]]))
|
|
59
|
+
|
|
60
|
+
# Models
|
|
61
|
+
models = provider.get("models", [])
|
|
62
|
+
if models:
|
|
63
|
+
with st.expander("Models"):
|
|
64
|
+
for model in models[:10]:
|
|
65
|
+
if isinstance(model, dict):
|
|
66
|
+
st.text(f"- {model.get('id', model.get('name', 'unknown'))}")
|
|
67
|
+
else:
|
|
68
|
+
st.text(f"- {model}")
|
|
69
|
+
if len(models) > 10:
|
|
70
|
+
st.caption(f"...and {len(models) - 10} more")
|
|
71
|
+
|
|
72
|
+
# Metadata
|
|
73
|
+
metadata = provider.get("metadata", {})
|
|
74
|
+
if metadata:
|
|
75
|
+
with st.expander("Details"):
|
|
76
|
+
for key, value in list(metadata.items())[:5]:
|
|
77
|
+
st.text(f"{key}: {value}")
|
|
78
|
+
|
|
79
|
+
# Refresh button
|
|
80
|
+
st.divider()
|
|
81
|
+
if st.button("Refresh Provider Status", use_container_width=True):
|
|
82
|
+
st.cache_data.clear()
|
|
83
|
+
st.rerun()
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""SDD Workflow page - spec progress, phase burndown, task tracking."""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
|
|
5
|
+
from foundry_mcp.dashboard.components.cards import kpi_row
|
|
6
|
+
from foundry_mcp.dashboard.components.charts import pie_chart, bar_chart, empty_chart
|
|
7
|
+
|
|
8
|
+
# Try importing pandas
|
|
9
|
+
try:
|
|
10
|
+
import pandas as pd
|
|
11
|
+
PANDAS_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
PANDAS_AVAILABLE = False
|
|
14
|
+
pd = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_specs():
|
|
18
|
+
"""Get list of specifications."""
|
|
19
|
+
try:
|
|
20
|
+
from foundry_mcp.core.spec import list_specs
|
|
21
|
+
from foundry_mcp.config import get_config
|
|
22
|
+
|
|
23
|
+
config = get_config()
|
|
24
|
+
specs_dir = config.specs_dir
|
|
25
|
+
return list_specs(specs_dir)
|
|
26
|
+
except ImportError:
|
|
27
|
+
return []
|
|
28
|
+
except Exception:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_spec_data(spec_id: str):
|
|
33
|
+
"""Get detailed spec data."""
|
|
34
|
+
try:
|
|
35
|
+
from foundry_mcp.core.spec import load_spec
|
|
36
|
+
from foundry_mcp.config import get_config
|
|
37
|
+
|
|
38
|
+
config = get_config()
|
|
39
|
+
return load_spec(config.specs_dir, spec_id)
|
|
40
|
+
except Exception:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _calculate_progress(spec_data: dict) -> dict:
|
|
45
|
+
"""Calculate progress metrics from spec data."""
|
|
46
|
+
if not spec_data:
|
|
47
|
+
return {"total": 0, "completed": 0, "in_progress": 0, "pending": 0, "blocked": 0, "percentage": 0}
|
|
48
|
+
|
|
49
|
+
tasks = spec_data.get("tasks", {})
|
|
50
|
+
status_counts = {"completed": 0, "in_progress": 0, "pending": 0, "blocked": 0}
|
|
51
|
+
|
|
52
|
+
for task_id, task in tasks.items():
|
|
53
|
+
status = task.get("status", "pending")
|
|
54
|
+
if status in status_counts:
|
|
55
|
+
status_counts[status] += 1
|
|
56
|
+
else:
|
|
57
|
+
status_counts["pending"] += 1
|
|
58
|
+
|
|
59
|
+
total = sum(status_counts.values())
|
|
60
|
+
percentage = (status_counts["completed"] / total * 100) if total > 0 else 0
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"total": total,
|
|
64
|
+
**status_counts,
|
|
65
|
+
"percentage": percentage,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def render():
|
|
70
|
+
"""Render the SDD Workflow page."""
|
|
71
|
+
st.header("SDD Workflow")
|
|
72
|
+
|
|
73
|
+
# Get specs
|
|
74
|
+
specs = _get_specs()
|
|
75
|
+
|
|
76
|
+
if not specs:
|
|
77
|
+
st.info("No specifications found.")
|
|
78
|
+
st.caption("Create specs using `foundry-cli spec create` or the MCP spec-create tool.")
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Spec selector
|
|
82
|
+
spec_options = {s.get("title", s.get("id", "unknown")): s.get("id") for s in specs if isinstance(s, dict)}
|
|
83
|
+
|
|
84
|
+
if not spec_options:
|
|
85
|
+
st.warning("Could not parse specification list")
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
selected_title = st.selectbox(
|
|
89
|
+
"Select Specification",
|
|
90
|
+
options=list(spec_options.keys()),
|
|
91
|
+
key="sdd_spec_selector",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
selected_id = spec_options.get(selected_title)
|
|
95
|
+
if not selected_id:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Load spec data
|
|
99
|
+
spec_data = _get_spec_data(selected_id)
|
|
100
|
+
if not spec_data:
|
|
101
|
+
st.warning(f"Could not load spec: {selected_id}")
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
# Calculate progress
|
|
105
|
+
progress = _calculate_progress(spec_data)
|
|
106
|
+
|
|
107
|
+
st.divider()
|
|
108
|
+
|
|
109
|
+
# Progress overview
|
|
110
|
+
st.subheader("Progress Overview")
|
|
111
|
+
|
|
112
|
+
# Progress bar
|
|
113
|
+
st.progress(progress["percentage"] / 100, text=f"Overall Progress: {progress['percentage']:.1f}%")
|
|
114
|
+
|
|
115
|
+
# KPI cards
|
|
116
|
+
kpi_row(
|
|
117
|
+
[
|
|
118
|
+
{"label": "Total Tasks", "value": progress["total"]},
|
|
119
|
+
{"label": "Completed", "value": progress["completed"]},
|
|
120
|
+
{"label": "In Progress", "value": progress["in_progress"]},
|
|
121
|
+
{"label": "Pending", "value": progress["pending"]},
|
|
122
|
+
{"label": "Blocked", "value": progress["blocked"]},
|
|
123
|
+
],
|
|
124
|
+
columns=5,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
st.divider()
|
|
128
|
+
|
|
129
|
+
# Charts row
|
|
130
|
+
col1, col2 = st.columns(2)
|
|
131
|
+
|
|
132
|
+
with col1:
|
|
133
|
+
st.subheader("Task Status Distribution")
|
|
134
|
+
if PANDAS_AVAILABLE and progress["total"] > 0:
|
|
135
|
+
status_df = pd.DataFrame([
|
|
136
|
+
{"status": "completed", "count": progress["completed"]},
|
|
137
|
+
{"status": "in_progress", "count": progress["in_progress"]},
|
|
138
|
+
{"status": "pending", "count": progress["pending"]},
|
|
139
|
+
{"status": "blocked", "count": progress["blocked"]},
|
|
140
|
+
])
|
|
141
|
+
status_df = status_df[status_df["count"] > 0] # Filter zero counts
|
|
142
|
+
|
|
143
|
+
pie_chart(
|
|
144
|
+
status_df,
|
|
145
|
+
values="count",
|
|
146
|
+
names="status",
|
|
147
|
+
title=None,
|
|
148
|
+
height=300,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
empty_chart("No task data")
|
|
152
|
+
|
|
153
|
+
with col2:
|
|
154
|
+
st.subheader("Phase Progress")
|
|
155
|
+
phases = spec_data.get("phases", [])
|
|
156
|
+
if PANDAS_AVAILABLE and phases:
|
|
157
|
+
phase_data = []
|
|
158
|
+
for phase in phases:
|
|
159
|
+
if isinstance(phase, dict):
|
|
160
|
+
phase_tasks = phase.get("tasks", [])
|
|
161
|
+
completed = len([t for t in phase_tasks if isinstance(t, dict) and t.get("status") == "completed"])
|
|
162
|
+
total = len(phase_tasks)
|
|
163
|
+
phase_data.append({
|
|
164
|
+
"phase": phase.get("title", phase.get("id", "unknown"))[:20],
|
|
165
|
+
"completed": completed,
|
|
166
|
+
"total": total,
|
|
167
|
+
"percentage": (completed / total * 100) if total > 0 else 0,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if phase_data:
|
|
171
|
+
phase_df = pd.DataFrame(phase_data)
|
|
172
|
+
bar_chart(
|
|
173
|
+
phase_df,
|
|
174
|
+
x="phase",
|
|
175
|
+
y="percentage",
|
|
176
|
+
title=None,
|
|
177
|
+
height=300,
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
empty_chart("No phase data")
|
|
181
|
+
else:
|
|
182
|
+
empty_chart("No phase data")
|
|
183
|
+
|
|
184
|
+
st.divider()
|
|
185
|
+
|
|
186
|
+
# Task list
|
|
187
|
+
st.subheader("Tasks")
|
|
188
|
+
tasks = spec_data.get("tasks", {})
|
|
189
|
+
|
|
190
|
+
if tasks:
|
|
191
|
+
# Status filter
|
|
192
|
+
status_filter = st.multiselect(
|
|
193
|
+
"Filter by Status",
|
|
194
|
+
options=["completed", "in_progress", "pending", "blocked"],
|
|
195
|
+
default=["in_progress", "pending", "blocked"],
|
|
196
|
+
key="task_status_filter",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Build task list
|
|
200
|
+
task_list = []
|
|
201
|
+
for task_id, task in tasks.items():
|
|
202
|
+
if isinstance(task, dict):
|
|
203
|
+
status = task.get("status", "pending")
|
|
204
|
+
if status in status_filter:
|
|
205
|
+
task_list.append({
|
|
206
|
+
"id": task_id,
|
|
207
|
+
"title": task.get("title", "Untitled"),
|
|
208
|
+
"status": status,
|
|
209
|
+
"estimated_hours": task.get("estimated_hours", 0),
|
|
210
|
+
"actual_hours": task.get("actual_hours", 0),
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
if task_list and PANDAS_AVAILABLE:
|
|
214
|
+
task_df = pd.DataFrame(task_list)
|
|
215
|
+
st.dataframe(
|
|
216
|
+
task_df,
|
|
217
|
+
use_container_width=True,
|
|
218
|
+
hide_index=True,
|
|
219
|
+
column_config={
|
|
220
|
+
"id": st.column_config.TextColumn("ID", width="small"),
|
|
221
|
+
"title": st.column_config.TextColumn("Title", width="large"),
|
|
222
|
+
"status": st.column_config.TextColumn("Status", width="small"),
|
|
223
|
+
"estimated_hours": st.column_config.NumberColumn("Est. Hours", format="%.1f", width="small"),
|
|
224
|
+
"actual_hours": st.column_config.NumberColumn("Act. Hours", format="%.1f", width="small"),
|
|
225
|
+
},
|
|
226
|
+
)
|
|
227
|
+
elif task_list:
|
|
228
|
+
for t in task_list:
|
|
229
|
+
st.text(f"- [{t['status']}] {t['title']}")
|
|
230
|
+
else:
|
|
231
|
+
st.info("No tasks matching filter")
|
|
232
|
+
else:
|
|
233
|
+
st.info("No tasks defined in this spec")
|
|
234
|
+
|
|
235
|
+
# Time tracking summary
|
|
236
|
+
st.divider()
|
|
237
|
+
st.subheader("Time Tracking")
|
|
238
|
+
|
|
239
|
+
total_estimated = sum(t.get("estimated_hours", 0) for t in tasks.values() if isinstance(t, dict))
|
|
240
|
+
total_actual = sum(t.get("actual_hours", 0) for t in tasks.values() if isinstance(t, dict))
|
|
241
|
+
variance = total_actual - total_estimated
|
|
242
|
+
|
|
243
|
+
col1, col2, col3 = st.columns(3)
|
|
244
|
+
with col1:
|
|
245
|
+
st.metric("Estimated Hours", f"{total_estimated:.1f}h")
|
|
246
|
+
with col2:
|
|
247
|
+
st.metric("Actual Hours", f"{total_actual:.1f}h")
|
|
248
|
+
with col3:
|
|
249
|
+
delta_color = "inverse" if variance > 0 else "normal"
|
|
250
|
+
st.metric(
|
|
251
|
+
"Variance",
|
|
252
|
+
f"{variance:+.1f}h",
|
|
253
|
+
delta=f"{(variance / total_estimated * 100):+.1f}%" if total_estimated > 0 else "N/A",
|
|
254
|
+
delta_color=delta_color,
|
|
255
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Tool Usage page - detailed breakdown of tool and action invocations."""
|
|
2
|
+
|
|
3
|
+
import streamlit as st
|
|
4
|
+
|
|
5
|
+
from foundry_mcp.dashboard.components.filters import time_range_filter
|
|
6
|
+
from foundry_mcp.dashboard.components.charts import bar_chart, pie_chart, empty_chart
|
|
7
|
+
from foundry_mcp.dashboard.components.cards import kpi_row
|
|
8
|
+
from foundry_mcp.dashboard.data.stores import (
|
|
9
|
+
get_tool_action_breakdown,
|
|
10
|
+
get_top_tool_actions,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# Try importing pandas
|
|
14
|
+
try:
|
|
15
|
+
import pandas as pd
|
|
16
|
+
PANDAS_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
PANDAS_AVAILABLE = False
|
|
19
|
+
pd = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render():
|
|
23
|
+
"""Render the Tool Usage page."""
|
|
24
|
+
st.header("Tool Usage")
|
|
25
|
+
|
|
26
|
+
# Time range filter
|
|
27
|
+
col1, col2 = st.columns([3, 1])
|
|
28
|
+
with col2:
|
|
29
|
+
hours = time_range_filter(key="tool_usage_time_range", default="24h")
|
|
30
|
+
|
|
31
|
+
st.divider()
|
|
32
|
+
|
|
33
|
+
# Get breakdown data
|
|
34
|
+
breakdown_df = get_tool_action_breakdown(since_hours=hours)
|
|
35
|
+
|
|
36
|
+
if breakdown_df is None or breakdown_df.empty:
|
|
37
|
+
st.warning("No tool usage data available. Data will appear once tools are invoked.")
|
|
38
|
+
st.info("Tool invocations are recorded when MCP tools are called.")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
# KPI Summary
|
|
42
|
+
total_invocations = int(breakdown_df["count"].sum())
|
|
43
|
+
unique_tools = breakdown_df["tool"].nunique()
|
|
44
|
+
unique_actions = breakdown_df[breakdown_df["action"] != "(no action)"]["action"].nunique()
|
|
45
|
+
success_count = int(breakdown_df[breakdown_df["status"] == "success"]["count"].sum())
|
|
46
|
+
success_rate = (success_count / total_invocations * 100) if total_invocations > 0 else 0
|
|
47
|
+
|
|
48
|
+
kpi_row(
|
|
49
|
+
[
|
|
50
|
+
{"label": "Total Invocations", "value": f"{total_invocations:,}"},
|
|
51
|
+
{"label": "Unique Tools", "value": unique_tools},
|
|
52
|
+
{"label": "Unique Actions", "value": unique_actions},
|
|
53
|
+
{"label": "Success Rate", "value": f"{success_rate:.1f}%"},
|
|
54
|
+
],
|
|
55
|
+
columns=4,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
st.divider()
|
|
59
|
+
|
|
60
|
+
# Charts Row
|
|
61
|
+
col1, col2 = st.columns(2)
|
|
62
|
+
|
|
63
|
+
with col1:
|
|
64
|
+
st.subheader("Top Tools")
|
|
65
|
+
tool_totals = breakdown_df.groupby("tool")["count"].sum().reset_index()
|
|
66
|
+
tool_totals = tool_totals.sort_values("count", ascending=True).tail(10)
|
|
67
|
+
if not tool_totals.empty:
|
|
68
|
+
bar_chart(tool_totals, x="count", y="tool", orientation="h", height=350)
|
|
69
|
+
else:
|
|
70
|
+
empty_chart("No tool data")
|
|
71
|
+
|
|
72
|
+
with col2:
|
|
73
|
+
st.subheader("Status Distribution")
|
|
74
|
+
status_totals = breakdown_df.groupby("status")["count"].sum().reset_index()
|
|
75
|
+
if not status_totals.empty:
|
|
76
|
+
pie_chart(status_totals, values="count", names="status", height=350)
|
|
77
|
+
else:
|
|
78
|
+
empty_chart("No status data")
|
|
79
|
+
|
|
80
|
+
st.divider()
|
|
81
|
+
|
|
82
|
+
# Detailed Action Breakdown
|
|
83
|
+
st.subheader("Action Breakdown by Tool")
|
|
84
|
+
|
|
85
|
+
# Tool selector
|
|
86
|
+
tools = sorted(breakdown_df["tool"].unique())
|
|
87
|
+
selected_tool = st.selectbox(
|
|
88
|
+
"Select Tool",
|
|
89
|
+
options=["All Tools"] + tools,
|
|
90
|
+
key="tool_usage_tool_selector",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if selected_tool == "All Tools":
|
|
94
|
+
filtered_df = breakdown_df
|
|
95
|
+
else:
|
|
96
|
+
filtered_df = breakdown_df[breakdown_df["tool"] == selected_tool]
|
|
97
|
+
|
|
98
|
+
if not filtered_df.empty and PANDAS_AVAILABLE:
|
|
99
|
+
# Create display name column
|
|
100
|
+
action_df = filtered_df.groupby(["tool", "action", "status"])["count"].sum().reset_index()
|
|
101
|
+
action_df["display_name"] = action_df.apply(
|
|
102
|
+
lambda r: f"{r['tool']}.{r['action']}" if r["action"] != "(no action)" else r["tool"],
|
|
103
|
+
axis=1
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Detailed table
|
|
107
|
+
st.subheader("Detailed Data")
|
|
108
|
+
display_df = action_df[["display_name", "status", "count"]].sort_values("count", ascending=False)
|
|
109
|
+
st.dataframe(
|
|
110
|
+
display_df,
|
|
111
|
+
use_container_width=True,
|
|
112
|
+
hide_index=True,
|
|
113
|
+
column_config={
|
|
114
|
+
"display_name": st.column_config.TextColumn("Tool.Action", width="medium"),
|
|
115
|
+
"status": st.column_config.TextColumn("Status", width="small"),
|
|
116
|
+
"count": st.column_config.NumberColumn("Invocations", width="small"),
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Export options
|
|
121
|
+
col1, col2 = st.columns(2)
|
|
122
|
+
with col1:
|
|
123
|
+
csv = display_df.to_csv(index=False)
|
|
124
|
+
st.download_button(
|
|
125
|
+
label="Download CSV",
|
|
126
|
+
data=csv,
|
|
127
|
+
file_name="tool_usage_export.csv",
|
|
128
|
+
mime="text/csv",
|
|
129
|
+
)
|
|
130
|
+
with col2:
|
|
131
|
+
json_data = display_df.to_json(orient="records")
|
|
132
|
+
st.download_button(
|
|
133
|
+
label="Download JSON",
|
|
134
|
+
data=json_data,
|
|
135
|
+
file_name="tool_usage_export.json",
|
|
136
|
+
mime="application/json",
|
|
137
|
+
)
|
|
138
|
+
else:
|
|
139
|
+
empty_chart("No data for selected filter")
|