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,195 @@
|
|
|
1
|
+
"""Data table components for dashboard."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
# Try importing pandas
|
|
8
|
+
try:
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
PANDAS_AVAILABLE = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
PANDAS_AVAILABLE = False
|
|
14
|
+
pd = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def data_table(
|
|
18
|
+
df: "pd.DataFrame",
|
|
19
|
+
columns: Optional[dict[str, Any]] = None,
|
|
20
|
+
height: Optional[int] = None,
|
|
21
|
+
selection_mode: Optional[str] = None,
|
|
22
|
+
on_select: Optional[callable] = None,
|
|
23
|
+
) -> Optional[dict]:
|
|
24
|
+
"""Render an interactive data table.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
df: DataFrame to display
|
|
28
|
+
columns: Column configuration dict for st.column_config
|
|
29
|
+
height: Optional fixed height
|
|
30
|
+
selection_mode: "single-row", "multi-row", or None
|
|
31
|
+
on_select: Callback for selection events
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Selection info if selection_mode is set, None otherwise
|
|
35
|
+
"""
|
|
36
|
+
if not PANDAS_AVAILABLE:
|
|
37
|
+
st.warning("Pandas not installed")
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if df is None or df.empty:
|
|
41
|
+
st.info("No data to display")
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
# Build kwargs
|
|
45
|
+
kwargs = {
|
|
46
|
+
"use_container_width": True,
|
|
47
|
+
"hide_index": True,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if columns:
|
|
51
|
+
kwargs["column_config"] = columns
|
|
52
|
+
|
|
53
|
+
if height:
|
|
54
|
+
kwargs["height"] = height
|
|
55
|
+
|
|
56
|
+
if selection_mode:
|
|
57
|
+
kwargs["selection_mode"] = selection_mode
|
|
58
|
+
event = st.dataframe(df, **kwargs, on_select="rerun")
|
|
59
|
+
return event
|
|
60
|
+
else:
|
|
61
|
+
st.dataframe(df, **kwargs)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def error_table_config() -> dict:
|
|
66
|
+
"""Get column configuration for error tables."""
|
|
67
|
+
return {
|
|
68
|
+
"id": st.column_config.TextColumn(
|
|
69
|
+
"ID",
|
|
70
|
+
width="small",
|
|
71
|
+
help="Error ID",
|
|
72
|
+
),
|
|
73
|
+
"timestamp": st.column_config.DatetimeColumn(
|
|
74
|
+
"Time",
|
|
75
|
+
format="HH:mm:ss",
|
|
76
|
+
width="small",
|
|
77
|
+
),
|
|
78
|
+
"tool_name": st.column_config.TextColumn(
|
|
79
|
+
"Tool",
|
|
80
|
+
width="medium",
|
|
81
|
+
),
|
|
82
|
+
"error_code": st.column_config.TextColumn(
|
|
83
|
+
"Code",
|
|
84
|
+
width="small",
|
|
85
|
+
),
|
|
86
|
+
"message": st.column_config.TextColumn(
|
|
87
|
+
"Message",
|
|
88
|
+
width="large",
|
|
89
|
+
),
|
|
90
|
+
"fingerprint": st.column_config.TextColumn(
|
|
91
|
+
"Pattern",
|
|
92
|
+
width="small",
|
|
93
|
+
help="Error fingerprint for pattern matching",
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def metrics_table_config() -> dict:
|
|
99
|
+
"""Get column configuration for metrics tables."""
|
|
100
|
+
return {
|
|
101
|
+
"metric_name": st.column_config.TextColumn(
|
|
102
|
+
"Metric",
|
|
103
|
+
width="medium",
|
|
104
|
+
),
|
|
105
|
+
"count": st.column_config.NumberColumn(
|
|
106
|
+
"Records",
|
|
107
|
+
width="small",
|
|
108
|
+
),
|
|
109
|
+
"first_seen": st.column_config.DatetimeColumn(
|
|
110
|
+
"First Seen",
|
|
111
|
+
format="MMM DD, HH:mm",
|
|
112
|
+
width="medium",
|
|
113
|
+
),
|
|
114
|
+
"last_seen": st.column_config.DatetimeColumn(
|
|
115
|
+
"Last Seen",
|
|
116
|
+
format="MMM DD, HH:mm",
|
|
117
|
+
width="medium",
|
|
118
|
+
),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def task_table_config() -> dict:
|
|
123
|
+
"""Get column configuration for task tables."""
|
|
124
|
+
return {
|
|
125
|
+
"id": st.column_config.TextColumn(
|
|
126
|
+
"ID",
|
|
127
|
+
width="small",
|
|
128
|
+
),
|
|
129
|
+
"title": st.column_config.TextColumn(
|
|
130
|
+
"Title",
|
|
131
|
+
width="large",
|
|
132
|
+
),
|
|
133
|
+
"status": st.column_config.TextColumn(
|
|
134
|
+
"Status",
|
|
135
|
+
width="small",
|
|
136
|
+
),
|
|
137
|
+
"estimated_hours": st.column_config.NumberColumn(
|
|
138
|
+
"Est. Hours",
|
|
139
|
+
format="%.1f",
|
|
140
|
+
width="small",
|
|
141
|
+
),
|
|
142
|
+
"actual_hours": st.column_config.NumberColumn(
|
|
143
|
+
"Act. Hours",
|
|
144
|
+
format="%.1f",
|
|
145
|
+
width="small",
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def paginated_table(
|
|
151
|
+
df: "pd.DataFrame",
|
|
152
|
+
page_size: int = 50,
|
|
153
|
+
key: str = "table_page",
|
|
154
|
+
columns: Optional[dict] = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Render a paginated data table.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
df: DataFrame to display
|
|
160
|
+
page_size: Rows per page
|
|
161
|
+
key: Unique key for pagination state
|
|
162
|
+
columns: Column configuration
|
|
163
|
+
"""
|
|
164
|
+
if not PANDAS_AVAILABLE:
|
|
165
|
+
st.warning("Pandas not installed")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
if df is None or df.empty:
|
|
169
|
+
st.info("No data to display")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
total_rows = len(df)
|
|
173
|
+
total_pages = (total_rows + page_size - 1) // page_size
|
|
174
|
+
|
|
175
|
+
# Page selector
|
|
176
|
+
col1, col2 = st.columns([3, 1])
|
|
177
|
+
with col2:
|
|
178
|
+
page = st.number_input(
|
|
179
|
+
"Page",
|
|
180
|
+
min_value=1,
|
|
181
|
+
max_value=max(1, total_pages),
|
|
182
|
+
value=1,
|
|
183
|
+
key=key,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
with col1:
|
|
187
|
+
st.caption(f"Showing {min(page_size, total_rows)} of {total_rows} rows")
|
|
188
|
+
|
|
189
|
+
# Slice data for current page
|
|
190
|
+
start_idx = (page - 1) * page_size
|
|
191
|
+
end_idx = start_idx + page_size
|
|
192
|
+
page_df = df.iloc[start_idx:end_idx]
|
|
193
|
+
|
|
194
|
+
# Display table
|
|
195
|
+
data_table(page_df, columns=columns)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Data access layer for dashboard.
|
|
2
|
+
|
|
3
|
+
Provides cached access to MetricsStore, ErrorStore, and other data sources,
|
|
4
|
+
returning pandas DataFrames for easy use with Streamlit components.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from foundry_mcp.dashboard.data import stores
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"stores",
|
|
11
|
+
]
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""Data access layer for dashboard.
|
|
2
|
+
|
|
3
|
+
Wraps MetricsStore, ErrorStore, and other data sources with:
|
|
4
|
+
- Singleton store instances (stores handle internal caching)
|
|
5
|
+
- pandas DataFrame conversion for easy use with st.dataframe
|
|
6
|
+
- Graceful handling when stores are disabled
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime, timedelta, timezone
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
import streamlit as st
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Try importing pandas - it's an optional dependency
|
|
20
|
+
try:
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
PANDAS_AVAILABLE = True
|
|
24
|
+
except ImportError:
|
|
25
|
+
PANDAS_AVAILABLE = False
|
|
26
|
+
pd = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_config():
|
|
30
|
+
"""Get foundry-mcp configuration."""
|
|
31
|
+
try:
|
|
32
|
+
from foundry_mcp.config import get_config
|
|
33
|
+
|
|
34
|
+
return get_config()
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logger.warning("Could not load config: %s", e)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_error_store():
|
|
41
|
+
"""Get error store singleton instance if enabled."""
|
|
42
|
+
config = _get_config()
|
|
43
|
+
if config is None or not config.error_collection.enabled:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from foundry_mcp.core.error_store import get_error_store
|
|
48
|
+
|
|
49
|
+
storage_path = config.error_collection.get_storage_path()
|
|
50
|
+
return get_error_store(storage_path) # Returns singleton
|
|
51
|
+
except Exception as e:
|
|
52
|
+
logger.warning("Could not initialize error store: %s", e)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_metrics_store():
|
|
57
|
+
"""Get metrics store singleton instance if enabled."""
|
|
58
|
+
config = _get_config()
|
|
59
|
+
if config is None or not config.metrics_persistence.enabled:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from foundry_mcp.core.metrics_store import get_metrics_store
|
|
64
|
+
|
|
65
|
+
storage_path = config.metrics_persistence.get_storage_path()
|
|
66
|
+
return get_metrics_store(storage_path) # Returns singleton
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.warning("Could not initialize metrics store: %s", e)
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# Error Data Functions
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_errors(
|
|
78
|
+
tool_name: Optional[str] = None,
|
|
79
|
+
error_code: Optional[str] = None,
|
|
80
|
+
since_hours: int = 24,
|
|
81
|
+
limit: int = 100,
|
|
82
|
+
) -> "pd.DataFrame":
|
|
83
|
+
"""Get errors as a DataFrame.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
tool_name: Filter by tool name
|
|
87
|
+
error_code: Filter by error code
|
|
88
|
+
since_hours: Hours to look back
|
|
89
|
+
limit: Maximum records to return
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
DataFrame with error records, or empty DataFrame if disabled
|
|
93
|
+
"""
|
|
94
|
+
if not PANDAS_AVAILABLE:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
store = _get_error_store()
|
|
98
|
+
if store is None:
|
|
99
|
+
return pd.DataFrame()
|
|
100
|
+
|
|
101
|
+
since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
records = store.query(
|
|
105
|
+
tool_name=tool_name,
|
|
106
|
+
error_code=error_code,
|
|
107
|
+
since=since,
|
|
108
|
+
limit=limit,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not records:
|
|
112
|
+
return pd.DataFrame()
|
|
113
|
+
|
|
114
|
+
# Convert to list of dicts
|
|
115
|
+
data = []
|
|
116
|
+
for r in records:
|
|
117
|
+
data.append(
|
|
118
|
+
{
|
|
119
|
+
"id": r.error_id,
|
|
120
|
+
"timestamp": r.timestamp,
|
|
121
|
+
"tool_name": r.tool_name,
|
|
122
|
+
"error_code": r.error_code,
|
|
123
|
+
"message": r.message,
|
|
124
|
+
"error_type": r.error_type,
|
|
125
|
+
"fingerprint": r.fingerprint,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
df = pd.DataFrame(data)
|
|
130
|
+
if "timestamp" in df.columns:
|
|
131
|
+
df["timestamp"] = pd.to_datetime(df["timestamp"])
|
|
132
|
+
return df
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.exception("Error querying errors: %s", e)
|
|
136
|
+
return pd.DataFrame()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_error_stats() -> dict[str, Any]:
|
|
140
|
+
"""Get error statistics."""
|
|
141
|
+
store = _get_error_store()
|
|
142
|
+
if store is None:
|
|
143
|
+
return {"enabled": False, "total": 0}
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
stats = store.get_stats()
|
|
147
|
+
stats["enabled"] = True
|
|
148
|
+
return stats
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.exception("Error getting error stats: %s", e)
|
|
151
|
+
return {"enabled": True, "error": str(e)}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_error_patterns(min_count: int = 3) -> list[dict]:
|
|
155
|
+
"""Get recurring error patterns."""
|
|
156
|
+
store = _get_error_store()
|
|
157
|
+
if store is None:
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
return store.get_patterns(min_count=min_count)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.exception("Error getting patterns: %s", e)
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_error_by_id(error_id: str) -> Optional[dict]:
|
|
168
|
+
"""Get a single error by ID (not cached for freshness)."""
|
|
169
|
+
store = _get_error_store()
|
|
170
|
+
if store is None:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
record = store.get(error_id)
|
|
175
|
+
if record is None:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"id": record.error_id,
|
|
180
|
+
"timestamp": record.timestamp,
|
|
181
|
+
"tool_name": record.tool_name,
|
|
182
|
+
"error_code": record.error_code,
|
|
183
|
+
"message": record.message,
|
|
184
|
+
"error_type": record.error_type,
|
|
185
|
+
"fingerprint": record.fingerprint,
|
|
186
|
+
"stack_trace": record.stack_trace,
|
|
187
|
+
"context": record.context,
|
|
188
|
+
}
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.exception("Error getting error by ID: %s", e)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# =============================================================================
|
|
195
|
+
# Metrics Data Functions
|
|
196
|
+
# =============================================================================
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def get_metrics_list() -> list[dict]:
|
|
200
|
+
"""Get list of available metrics."""
|
|
201
|
+
store = _get_metrics_store()
|
|
202
|
+
if store is None:
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
return store.list_metrics()
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.exception("Error listing metrics: %s", e)
|
|
209
|
+
return []
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_metrics_timeseries(
|
|
213
|
+
metric_name: str,
|
|
214
|
+
since_hours: int = 24,
|
|
215
|
+
limit: int = 1000,
|
|
216
|
+
) -> "pd.DataFrame":
|
|
217
|
+
"""Get time-series data for a metric.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
metric_name: Name of the metric
|
|
221
|
+
since_hours: Hours to look back
|
|
222
|
+
limit: Maximum data points
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
DataFrame with timestamp and value columns
|
|
226
|
+
"""
|
|
227
|
+
if not PANDAS_AVAILABLE:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
store = _get_metrics_store()
|
|
231
|
+
if store is None:
|
|
232
|
+
return pd.DataFrame()
|
|
233
|
+
|
|
234
|
+
since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
data_points = store.query(
|
|
238
|
+
metric_name=metric_name,
|
|
239
|
+
since=since,
|
|
240
|
+
limit=limit,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not data_points:
|
|
244
|
+
return pd.DataFrame()
|
|
245
|
+
|
|
246
|
+
data = [
|
|
247
|
+
{
|
|
248
|
+
"timestamp": dp.timestamp,
|
|
249
|
+
"value": dp.value,
|
|
250
|
+
"labels": str(dp.labels) if dp.labels else "",
|
|
251
|
+
}
|
|
252
|
+
for dp in data_points
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
df = pd.DataFrame(data)
|
|
256
|
+
if "timestamp" in df.columns:
|
|
257
|
+
df["timestamp"] = pd.to_datetime(df["timestamp"])
|
|
258
|
+
return df
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
logger.exception("Error querying metrics: %s", e)
|
|
262
|
+
return pd.DataFrame()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_metrics_summary(metric_name: str, since_hours: int = 24) -> dict[str, Any]:
|
|
266
|
+
"""Get summary statistics for a metric."""
|
|
267
|
+
store = _get_metrics_store()
|
|
268
|
+
if store is None:
|
|
269
|
+
return {"enabled": False}
|
|
270
|
+
|
|
271
|
+
since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
summary = store.get_summary(metric_name, since=since)
|
|
275
|
+
summary["enabled"] = True
|
|
276
|
+
return summary
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.exception("Error getting summary: %s", e)
|
|
279
|
+
return {"enabled": True, "error": str(e)}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# =============================================================================
|
|
283
|
+
# Tool Usage Breakdown Functions
|
|
284
|
+
# =============================================================================
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_tool_action_breakdown(
|
|
288
|
+
since_hours: int = 24,
|
|
289
|
+
limit: int = 1000,
|
|
290
|
+
) -> "pd.DataFrame":
|
|
291
|
+
"""Get tool invocations broken down by tool and action.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
since_hours: Hours to look back
|
|
295
|
+
limit: Maximum data points to query
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
DataFrame with tool, action, status, and count columns
|
|
299
|
+
"""
|
|
300
|
+
if not PANDAS_AVAILABLE:
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
store = _get_metrics_store()
|
|
304
|
+
if store is None:
|
|
305
|
+
return pd.DataFrame()
|
|
306
|
+
|
|
307
|
+
since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
data_points = store.query(
|
|
311
|
+
metric_name="tool_invocations_total",
|
|
312
|
+
since=since,
|
|
313
|
+
limit=limit,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if not data_points:
|
|
317
|
+
return pd.DataFrame()
|
|
318
|
+
|
|
319
|
+
# Group by tool, action, status
|
|
320
|
+
breakdown: dict[tuple[str, str, str], float] = {}
|
|
321
|
+
for dp in data_points:
|
|
322
|
+
tool = dp.labels.get("tool", "unknown")
|
|
323
|
+
action = dp.labels.get("action", "") # Empty string for legacy data
|
|
324
|
+
status = dp.labels.get("status", "unknown")
|
|
325
|
+
key = (tool, action, status)
|
|
326
|
+
breakdown[key] = breakdown.get(key, 0) + dp.value
|
|
327
|
+
|
|
328
|
+
data = [
|
|
329
|
+
{"tool": k[0], "action": k[1] or "(no action)", "status": k[2], "count": int(v)}
|
|
330
|
+
for k, v in breakdown.items()
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
return pd.DataFrame(data)
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
logger.exception("Error getting tool action breakdown: %s", e)
|
|
337
|
+
return pd.DataFrame()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def get_top_tool_actions(since_hours: int = 24, top_n: int = 10) -> list[dict]:
|
|
341
|
+
"""Get top N most called tool+action combinations.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
since_hours: Hours to look back
|
|
345
|
+
top_n: Number of top items to return
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
List of dicts with tool, action, count
|
|
349
|
+
"""
|
|
350
|
+
df = get_tool_action_breakdown(since_hours=since_hours)
|
|
351
|
+
if df is None or df.empty:
|
|
352
|
+
return []
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Group by tool+action, sum counts across statuses
|
|
356
|
+
grouped = df.groupby(["tool", "action"])["count"].sum().reset_index()
|
|
357
|
+
grouped = grouped.sort_values("count", ascending=False).head(top_n)
|
|
358
|
+
|
|
359
|
+
return grouped.to_dict("records")
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.exception("Error getting top tool actions: %s", e)
|
|
362
|
+
return []
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
# =============================================================================
|
|
366
|
+
# Health & Provider Data Functions
|
|
367
|
+
# =============================================================================
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@st.cache_data(ttl=10)
|
|
371
|
+
def get_health_status() -> dict[str, Any]:
|
|
372
|
+
"""Get server health status."""
|
|
373
|
+
try:
|
|
374
|
+
from foundry_mcp.core.health import get_health_manager
|
|
375
|
+
|
|
376
|
+
manager = get_health_manager()
|
|
377
|
+
result = manager.check_health()
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
"healthy": result.healthy,
|
|
381
|
+
"status": result.status.value if hasattr(result.status, "value") else str(result.status),
|
|
382
|
+
"checks": {
|
|
383
|
+
name: {
|
|
384
|
+
"healthy": check.healthy,
|
|
385
|
+
"message": check.message,
|
|
386
|
+
}
|
|
387
|
+
for name, check in (result.checks or {}).items()
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
except Exception as e:
|
|
391
|
+
logger.exception("Error getting health: %s", e)
|
|
392
|
+
return {"healthy": False, "error": str(e)}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@st.cache_data(ttl=30)
|
|
396
|
+
def get_providers() -> list[dict]:
|
|
397
|
+
"""Get list of AI providers with status."""
|
|
398
|
+
try:
|
|
399
|
+
from foundry_mcp.core.providers import describe_providers
|
|
400
|
+
|
|
401
|
+
return describe_providers()
|
|
402
|
+
except ImportError:
|
|
403
|
+
# Providers module may not exist yet
|
|
404
|
+
return []
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.exception("Error getting providers: %s", e)
|
|
407
|
+
return []
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# =============================================================================
|
|
411
|
+
# Overview Summary Functions
|
|
412
|
+
# =============================================================================
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def get_overview_summary() -> dict[str, Any]:
|
|
416
|
+
"""Get aggregated overview metrics for dashboard."""
|
|
417
|
+
summary = {
|
|
418
|
+
"total_invocations": 0,
|
|
419
|
+
"error_count": 0,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Get metrics summary
|
|
423
|
+
metrics_list = get_metrics_list()
|
|
424
|
+
for m in metrics_list:
|
|
425
|
+
if m.get("metric_name") == "tool_invocations_total":
|
|
426
|
+
summary["total_invocations"] = m.get("count", 0)
|
|
427
|
+
|
|
428
|
+
# Get error count from store (single source of truth)
|
|
429
|
+
error_store = _get_error_store()
|
|
430
|
+
if error_store is not None:
|
|
431
|
+
summary["error_count"] = error_store.get_total_count()
|
|
432
|
+
|
|
433
|
+
return summary
|