flowyml 1.7.2__py3-none-any.whl → 1.8.0__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.
Files changed (126) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/metrics.py +5 -0
  3. flowyml/cli/main.py +709 -0
  4. flowyml/cli/stack_cli.py +138 -25
  5. flowyml/core/__init__.py +17 -0
  6. flowyml/core/executor.py +161 -26
  7. flowyml/core/image_builder.py +129 -0
  8. flowyml/core/log_streamer.py +227 -0
  9. flowyml/core/orchestrator.py +22 -2
  10. flowyml/core/pipeline.py +34 -10
  11. flowyml/core/routing.py +558 -0
  12. flowyml/core/step.py +9 -1
  13. flowyml/core/step_grouping.py +49 -35
  14. flowyml/core/types.py +407 -0
  15. flowyml/monitoring/alerts.py +10 -0
  16. flowyml/monitoring/notifications.py +104 -25
  17. flowyml/monitoring/slack_blocks.py +323 -0
  18. flowyml/plugins/__init__.py +251 -0
  19. flowyml/plugins/alerters/__init__.py +1 -0
  20. flowyml/plugins/alerters/slack.py +168 -0
  21. flowyml/plugins/base.py +752 -0
  22. flowyml/plugins/config.py +478 -0
  23. flowyml/plugins/deployers/__init__.py +22 -0
  24. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  25. flowyml/plugins/deployers/sagemaker.py +306 -0
  26. flowyml/plugins/deployers/vertex.py +290 -0
  27. flowyml/plugins/integration.py +369 -0
  28. flowyml/plugins/manager.py +510 -0
  29. flowyml/plugins/model_registries/__init__.py +22 -0
  30. flowyml/plugins/model_registries/mlflow.py +159 -0
  31. flowyml/plugins/model_registries/sagemaker.py +489 -0
  32. flowyml/plugins/model_registries/vertex.py +386 -0
  33. flowyml/plugins/orchestrators/__init__.py +13 -0
  34. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  35. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  36. flowyml/plugins/registries/__init__.py +13 -0
  37. flowyml/plugins/registries/ecr.py +321 -0
  38. flowyml/plugins/registries/gcr.py +313 -0
  39. flowyml/plugins/registry.py +454 -0
  40. flowyml/plugins/stack.py +494 -0
  41. flowyml/plugins/stack_config.py +537 -0
  42. flowyml/plugins/stores/__init__.py +13 -0
  43. flowyml/plugins/stores/gcs.py +460 -0
  44. flowyml/plugins/stores/s3.py +453 -0
  45. flowyml/plugins/trackers/__init__.py +11 -0
  46. flowyml/plugins/trackers/mlflow.py +316 -0
  47. flowyml/plugins/validators/__init__.py +3 -0
  48. flowyml/plugins/validators/deepchecks.py +119 -0
  49. flowyml/registry/__init__.py +2 -1
  50. flowyml/registry/model_environment.py +109 -0
  51. flowyml/registry/model_registry.py +241 -96
  52. flowyml/serving/__init__.py +17 -0
  53. flowyml/serving/model_server.py +628 -0
  54. flowyml/stacks/__init__.py +60 -0
  55. flowyml/stacks/aws.py +93 -0
  56. flowyml/stacks/base.py +62 -0
  57. flowyml/stacks/components.py +12 -0
  58. flowyml/stacks/gcp.py +44 -9
  59. flowyml/stacks/plugins.py +115 -0
  60. flowyml/stacks/registry.py +2 -1
  61. flowyml/storage/sql.py +401 -12
  62. flowyml/tracking/experiment.py +8 -5
  63. flowyml/ui/backend/Dockerfile +87 -16
  64. flowyml/ui/backend/auth.py +12 -2
  65. flowyml/ui/backend/main.py +149 -5
  66. flowyml/ui/backend/routers/ai_context.py +226 -0
  67. flowyml/ui/backend/routers/assets.py +23 -4
  68. flowyml/ui/backend/routers/auth.py +96 -0
  69. flowyml/ui/backend/routers/deployments.py +660 -0
  70. flowyml/ui/backend/routers/model_explorer.py +597 -0
  71. flowyml/ui/backend/routers/plugins.py +103 -51
  72. flowyml/ui/backend/routers/projects.py +91 -8
  73. flowyml/ui/backend/routers/runs.py +20 -1
  74. flowyml/ui/backend/routers/schedules.py +22 -17
  75. flowyml/ui/backend/routers/templates.py +319 -0
  76. flowyml/ui/backend/routers/websocket.py +2 -2
  77. flowyml/ui/frontend/Dockerfile +55 -6
  78. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  79. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  80. flowyml/ui/frontend/dist/index.html +2 -2
  81. flowyml/ui/frontend/dist/logo.png +0 -0
  82. flowyml/ui/frontend/nginx.conf +65 -4
  83. flowyml/ui/frontend/package-lock.json +1404 -74
  84. flowyml/ui/frontend/package.json +3 -0
  85. flowyml/ui/frontend/public/logo.png +0 -0
  86. flowyml/ui/frontend/src/App.jsx +10 -7
  87. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  88. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  89. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  90. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  91. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  92. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  93. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
  94. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  95. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  96. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
  97. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  98. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  99. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  100. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  101. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  102. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  103. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  104. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  105. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  106. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  107. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  108. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  109. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  110. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  111. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  112. flowyml/ui/frontend/src/router/index.jsx +47 -20
  113. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  114. flowyml/ui/server_manager.py +5 -5
  115. flowyml/ui/utils.py +157 -39
  116. flowyml/utils/config.py +37 -15
  117. flowyml/utils/model_introspection.py +123 -0
  118. flowyml/utils/observability.py +30 -0
  119. flowyml-1.8.0.dist-info/METADATA +174 -0
  120. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
  121. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  122. flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
  123. flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
  124. flowyml-1.7.2.dist-info/METADATA +0 -477
  125. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  126. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -8,7 +8,18 @@ import traceback
8
8
 
9
9
  from flowyml.monitoring.alerts import alert_manager, AlertLevel
10
10
 
11
- # Include API routers
11
+ # OpenTelemetry Imports
12
+ from opentelemetry import trace, metrics
13
+ from opentelemetry.sdk.trace import TracerProvider
14
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
15
+ from opentelemetry.sdk.metrics import MeterProvider
16
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
17
+ from opentelemetry.exporter.prometheus import PrometheusMetricReader
18
+ from opentelemetry.sdk.resources import SERVICE_NAME, Resource
19
+ from prometheus_client import make_asgi_app
20
+ from starlette.middleware.base import BaseHTTPMiddleware
21
+ from starlette.requests import Request
22
+
12
23
  from flowyml.ui.backend.routers import (
13
24
  pipelines,
14
25
  runs,
@@ -21,28 +32,149 @@ from flowyml.ui.backend.routers import (
21
32
  leaderboard,
22
33
  execution,
23
34
  plugins,
24
- metrics,
35
+ metrics as metrics_router,
25
36
  client,
26
37
  stats,
27
38
  websocket,
39
+ deployments,
40
+ model_explorer,
41
+ auth, # New Auth Router
42
+ templates, # Pipeline Templates
43
+ ai_context, # AI Assistant Context
44
+ )
45
+
46
+ # Initialize OpenTelemetry
47
+ resource = Resource(
48
+ attributes={
49
+ SERVICE_NAME: "flowyml-backend",
50
+ },
28
51
  )
29
52
 
53
+ # Tracing - Use OTLP exporter in production, console in development
54
+ trace_provider = TracerProvider(resource=resource)
55
+ _otlp_endpoint = os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
56
+ if _otlp_endpoint:
57
+ # Production: Use OTLP exporter (supports Jaeger, Honeycomb, Google Cloud Trace, etc.)
58
+ try:
59
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
60
+
61
+ trace_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=_otlp_endpoint)))
62
+ except ImportError:
63
+ # Fallback to console if OTLP exporter not installed
64
+ trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
65
+ else:
66
+ # Development: Console exporter for debugging
67
+ trace_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
68
+ trace.set_tracer_provider(trace_provider)
69
+
70
+ # Metrics (Prometheus)
71
+ reader = PrometheusMetricReader()
72
+ meter_provider = MeterProvider(resource=resource, metric_readers=[reader])
73
+ metrics.set_meter_provider(meter_provider)
74
+
75
+ # Routers included above
76
+
30
77
  app = FastAPI(
31
78
  title="flowyml UI",
32
79
  description="Real-time UI for flowyml pipelines",
33
80
  version="0.1.0",
34
81
  )
35
82
 
36
- # Configure CORS
83
+ # Instrument FastAPI
84
+ FastAPIInstrumentor.instrument_app(app)
85
+
86
+ # Expose Prometheus metrics
87
+ metrics_app = make_asgi_app()
88
+ app.mount("/metrics", metrics_app)
89
+
90
+ # Configure CORS - Production settings with localhost for development
91
+ _is_production = os.getenv("FLOWYML_ENV") == "production"
92
+ _cors_origins = (
93
+ [
94
+ # Production: Only allow specific origins
95
+ "https://flowyml.unicolab.ai",
96
+ "https://app.flowyml.io",
97
+ ]
98
+ if _is_production
99
+ else [
100
+ # Development: Allow localhost and common dev ports
101
+ "http://localhost:3000",
102
+ "http://localhost:5173",
103
+ "http://localhost:8080",
104
+ "http://127.0.0.1:3000",
105
+ "http://127.0.0.1:5173",
106
+ "http://127.0.0.1:8080",
107
+ "*", # Fallback for dev flexibility
108
+ ]
109
+ )
110
+
37
111
  app.add_middleware(
38
112
  CORSMiddleware,
39
- allow_origins=["*"], # For development
113
+ allow_origins=_cors_origins,
40
114
  allow_credentials=True,
41
115
  allow_methods=["*"],
42
116
  allow_headers=["*"],
43
117
  )
44
118
 
45
119
 
120
+ class AuthMiddleware(BaseHTTPMiddleware):
121
+ async def dispatch(self, request: Request, call_next):
122
+ # 0. Skip if not in production to allow easy local development
123
+ if os.getenv("FLOWYML_ENV") != "production":
124
+ return await call_next(request)
125
+
126
+ # 1. Check if token auth is enabled via env var
127
+ api_token = os.getenv("FLOWYML_API_TOKEN")
128
+ if not api_token:
129
+ return await call_next(request)
130
+
131
+ # 2. Define public paths
132
+ path = request.url.path
133
+ if (
134
+ path in ["/api/health", "/metrics", "/docs", "/redoc", "/openapi.json"]
135
+ or path.startswith(("/assets", "/api/auth/login", "/api/auth/logout")) # Allow login endpoints
136
+ or path == "/"
137
+ or request.method == "OPTIONS"
138
+ ):
139
+ return await call_next(request)
140
+
141
+ # 3. Check Auth Header OR Cookie
142
+ auth_header = request.headers.get("Authorization")
143
+ if not auth_header:
144
+ # Check for cookie
145
+ cookie_token = request.cookies.get("access_token")
146
+ if cookie_token:
147
+ auth_header = cookie_token # Reuse validation logic below
148
+ else:
149
+ token_param = request.query_params.get("token")
150
+ if token_param == api_token:
151
+ return await call_next(request)
152
+
153
+ return JSONResponse(
154
+ status_code=401,
155
+ content={"error": "Unauthorized", "message": "Missing authentication token"},
156
+ )
157
+
158
+ # 4. Validate Token
159
+ try:
160
+ scheme, token = auth_header.split()
161
+ if scheme.lower() != "bearer" or token != api_token:
162
+ return JSONResponse(
163
+ status_code=401,
164
+ content={"error": "Unauthorized", "message": "Invalid authentication token"},
165
+ )
166
+ except ValueError:
167
+ return JSONResponse(
168
+ status_code=401,
169
+ content={"error": "Unauthorized", "message": "Invalid Authorization header format"},
170
+ )
171
+
172
+ return await call_next(request)
173
+
174
+
175
+ app.add_middleware(AuthMiddleware)
176
+
177
+
46
178
  # Health check endpoint
47
179
  @app.get("/api/health")
48
180
  async def health_check():
@@ -74,11 +206,16 @@ app.include_router(schedules.router, prefix="/api/schedules", tags=["schedules"]
74
206
  app.include_router(notifications.router, prefix="/api/notifications", tags=["notifications"])
75
207
  app.include_router(leaderboard.router, prefix="/api/leaderboard", tags=["leaderboard"])
76
208
  app.include_router(execution.router, prefix="/api/execution", tags=["execution"])
77
- app.include_router(metrics.router, prefix="/api/metrics", tags=["metrics"])
209
+ app.include_router(metrics_router.router, prefix="/api/metrics", tags=["metrics"])
78
210
  app.include_router(plugins.router, prefix="/api", tags=["plugins"])
79
211
  app.include_router(client.router, prefix="/api/client", tags=["client"])
80
212
  app.include_router(stats.router, prefix="/api/stats", tags=["stats"])
81
213
  app.include_router(websocket.router, tags=["websocket"])
214
+ app.include_router(deployments.router, prefix="/api", tags=["deployments"])
215
+ app.include_router(model_explorer.router, prefix="/api", tags=["model-explorer"])
216
+ app.include_router(auth.router, prefix="/api", tags=["auth"])
217
+ app.include_router(templates.router, tags=["templates"])
218
+ app.include_router(ai_context.router, tags=["ai"]) # AI Context for assistant
82
219
 
83
220
 
84
221
  # Static file serving for frontend
@@ -150,3 +287,10 @@ async def validation_exception_handler(request, exc):
150
287
  status_code=422,
151
288
  content={"error": "Validation Error", "detail": exc.errors()},
152
289
  )
290
+
291
+
292
+ if __name__ == "__main__":
293
+ import uvicorn
294
+
295
+ port = int(os.environ.get("PORT", "8080"))
296
+ uvicorn.run("flowyml.ui.backend.main:app", host="0.0.0.0", port=port, reload=False) # noqa: S104
@@ -0,0 +1,226 @@
1
+ """
2
+ AI Context Router - Provides comprehensive context for the AI assistant.
3
+
4
+ This endpoint aggregates run details, logs, metrics, and step code to provide
5
+ rich context for the in-browser AI assistant.
6
+ """
7
+
8
+ from fastapi import APIRouter, HTTPException
9
+ from pydantic import BaseModel
10
+ from ..dependencies import get_store
11
+ import json
12
+ from flowyml.utils.config import get_config
13
+
14
+ router = APIRouter(prefix="/api/ai", tags=["ai"])
15
+
16
+
17
+ class AIContextRequest(BaseModel):
18
+ """Request model for AI context."""
19
+
20
+ page_type: str # 'run', 'pipeline', 'experiment', 'asset'
21
+ resource_id: str # run_id, pipeline_name, etc.
22
+ include_logs: bool = True
23
+ include_code: bool = True
24
+ include_metrics: bool = True
25
+ max_log_lines: int = 100
26
+
27
+
28
+ class AIContextResponse(BaseModel):
29
+ """Response model with comprehensive AI context."""
30
+
31
+ page_type: str
32
+ resource_id: str
33
+ summary: dict
34
+ details: dict
35
+ suggestions: list[str] = []
36
+
37
+
38
+ def _summarize_run(run: dict, include_logs: bool = True, include_code: bool = True, max_log_lines: int = 100) -> dict:
39
+ """Generate a comprehensive summary of a run for AI context."""
40
+ steps_info = []
41
+ failed_steps = []
42
+
43
+ steps = run.get("steps", {})
44
+ if isinstance(steps, str):
45
+ try:
46
+ steps = json.loads(steps)
47
+ except Exception:
48
+ steps = {}
49
+
50
+ if steps:
51
+ for name, step_data in steps.items():
52
+ step_summary = {
53
+ "name": name,
54
+ "status": "success"
55
+ if step_data.get("success")
56
+ else ("failed" if step_data.get("error") else "pending"),
57
+ "duration": f"{step_data.get('duration', 0):.2f}s",
58
+ "cached": step_data.get("cached", False),
59
+ }
60
+
61
+ # Include error details for failed steps
62
+ if step_data.get("error"):
63
+ step_summary["error"] = str(step_data["error"])[:500] # Truncate long errors
64
+ failed_steps.append(name)
65
+
66
+ # Include source code if requested
67
+ if include_code and step_data.get("source_code"):
68
+ step_summary["source_code"] = step_data["source_code"][:2000] # Limit code size
69
+
70
+ # Include inputs/outputs
71
+ step_summary["inputs"] = step_data.get("inputs", [])[:5]
72
+ step_summary["outputs"] = step_data.get("outputs", [])[:5]
73
+
74
+ steps_info.append(step_summary)
75
+
76
+ summary = {
77
+ "run_id": str(run.get("run_id")),
78
+ "pipeline_name": run.get("pipeline_name"),
79
+ "status": run.get("status"),
80
+ "duration": f"{run.get('duration', 0):.2f}s" if run.get("duration") else None,
81
+ "start_time": run.get("start_time"),
82
+ "end_time": run.get("end_time"),
83
+ "total_steps": len(steps_info),
84
+ "successful_steps": len([s for s in steps_info if s["status"] == "success"]),
85
+ "failed_steps": len(failed_steps),
86
+ "cached_steps": len([s for s in steps_info if s["cached"]]),
87
+ "steps": steps_info,
88
+ "failed_step_names": failed_steps,
89
+ "context_params": run.get("context") or {},
90
+ "environment": run.get("environment") or {},
91
+ }
92
+
93
+ return summary
94
+
95
+
96
+ def _get_run_logs(run_id: str, max_lines: int = 100) -> dict:
97
+ """Fetch recent logs for a run from filesystem."""
98
+ logs_by_step = {}
99
+ runs_dir = get_config().runs_dir
100
+ log_dir = runs_dir / run_id / "logs"
101
+
102
+ if not log_dir.exists():
103
+ return {}
104
+
105
+ for log_file in log_dir.glob("*.log"):
106
+ step_name = log_file.stem
107
+ try:
108
+ with open(log_file) as f:
109
+ lines = f.readlines()
110
+ # Get last N lines
111
+ recent_lines = lines[-max_lines:]
112
+ logs_by_step[step_name] = [{"message": line.strip(), "level": "INFO"} for line in recent_lines]
113
+ except Exception:
114
+ continue
115
+
116
+ return logs_by_step
117
+
118
+
119
+ def _get_run_metrics(run_id: str) -> list:
120
+ """Fetch metrics for a run."""
121
+ metrics = []
122
+ store = get_store()
123
+
124
+ db_metrics = store.get_metrics(run_id)
125
+ if db_metrics:
126
+ for m in db_metrics[:50]:
127
+ metrics.append(
128
+ {
129
+ "name": m.get("name"),
130
+ "value": m.get("value"),
131
+ "step": m.get("step"),
132
+ },
133
+ )
134
+
135
+ return metrics
136
+
137
+
138
+ def _generate_suggestions(summary: dict) -> list[str]:
139
+ """Generate AI-friendly suggestions based on run data."""
140
+ suggestions = []
141
+
142
+ if summary.get("failed_steps", 0) > 0:
143
+ suggestions.append(f"Analyze the {summary['failed_steps']} failed step(s) and suggest fixes")
144
+
145
+ if summary.get("cached_steps", 0) == 0 and summary.get("total_steps", 0) > 3:
146
+ suggestions.append("Consider enabling caching for frequently-run steps")
147
+
148
+ duration_str = summary.get("duration")
149
+ if duration_str:
150
+ try:
151
+ duration = float(duration_str.replace("s", ""))
152
+ if duration > 300:
153
+ suggestions.append("The run took over 5 minutes - consider optimization opportunities")
154
+ except (ValueError, AttributeError):
155
+ pass
156
+
157
+ return suggestions
158
+
159
+
160
+ @router.post("/context", response_model=AIContextResponse)
161
+ async def get_ai_context(request: AIContextRequest):
162
+ """
163
+ Get comprehensive AI context for a specific page/resource.
164
+
165
+ This endpoint aggregates all relevant information that the AI assistant
166
+ might need to provide helpful, context-aware responses.
167
+ """
168
+ if request.page_type == "run":
169
+ # Fetch run data
170
+ store = get_store()
171
+ run = store.load_run(request.resource_id)
172
+
173
+ if not run:
174
+ raise HTTPException(status_code=404, detail="Run not found")
175
+
176
+ # Generate comprehensive summary
177
+ summary = _summarize_run(
178
+ run,
179
+ include_logs=request.include_logs,
180
+ include_code=request.include_code,
181
+ max_log_lines=request.max_log_lines,
182
+ )
183
+
184
+ # Fetch additional data
185
+ details = {}
186
+
187
+ if request.include_logs:
188
+ details["logs"] = _get_run_logs(request.resource_id, request.max_log_lines)
189
+
190
+ if request.include_metrics:
191
+ details["metrics"] = _get_run_metrics(request.resource_id)
192
+
193
+ # Generate suggestions
194
+ suggestions = _generate_suggestions(summary)
195
+
196
+ return AIContextResponse(
197
+ page_type=request.page_type,
198
+ resource_id=request.resource_id,
199
+ summary=summary,
200
+ details=details,
201
+ suggestions=suggestions,
202
+ )
203
+
204
+ # Add more page types as needed
205
+ raise HTTPException(status_code=400, detail=f"Unsupported page type: {request.page_type}")
206
+
207
+
208
+ @router.get("/context/run/{run_id}")
209
+ async def get_run_ai_context(
210
+ run_id: str,
211
+ include_logs: bool = True,
212
+ include_code: bool = True,
213
+ include_metrics: bool = True,
214
+ ):
215
+ """
216
+ Convenience endpoint for getting AI context for a specific run.
217
+ """
218
+ return await get_ai_context(
219
+ AIContextRequest(
220
+ page_type="run",
221
+ resource_id=run_id,
222
+ include_logs=include_logs,
223
+ include_code=include_code,
224
+ include_metrics=include_metrics,
225
+ ),
226
+ )
@@ -1,7 +1,6 @@
1
1
  from fastapi import APIRouter, HTTPException, UploadFile, File
2
2
  from fastapi.responses import FileResponse
3
3
  from pydantic import BaseModel, Field
4
- from flowyml.storage.metadata import SQLiteMetadataStore
5
4
  from flowyml.core.project import ProjectManager
6
5
  from pathlib import Path
7
6
  from flowyml.ui.backend.dependencies import get_store
@@ -18,7 +17,8 @@ def _save_file_sync(src, dst):
18
17
 
19
18
 
20
19
  def _iter_metadata_stores():
21
- stores = [(None, SQLiteMetadataStore())]
20
+ # Use the main configured store (PostgreSQL in Docker deployment)
21
+ stores = [(None, get_store())]
22
22
  try:
23
23
  manager = ProjectManager()
24
24
  for project_meta in manager.list_projects():
@@ -51,6 +51,14 @@ async def list_assets(limit: int = 50, asset_type: str = None, run_id: str = Non
51
51
  """List all assets, optionally filtered by project."""
52
52
  try:
53
53
  combined = []
54
+ project_run_ids = set()
55
+
56
+ # If filtering by project, first get all run_ids for that project
57
+ if project:
58
+ main_store = get_store()
59
+ all_runs = main_store.list_runs(limit=1000)
60
+ project_run_ids = {r.get("run_id") for r in all_runs if r.get("project") == project}
61
+
54
62
  for project_name, store in _iter_metadata_stores():
55
63
  if project and project_name and project != project_name:
56
64
  continue
@@ -61,14 +69,25 @@ async def list_assets(limit: int = 50, asset_type: str = None, run_id: str = Non
61
69
  if run_id:
62
70
  filters["run_id"] = run_id
63
71
 
64
- assets = store.list_assets(limit=limit, **filters)
72
+ assets = store.list_assets(limit=limit * 2, **filters) # Get more to filter
65
73
  for asset in assets:
66
74
  combined.append((asset, project_name))
67
75
 
68
76
  assets = _dedupe_assets(combined)
69
77
 
78
+ # Enhanced filtering: include artifacts that either:
79
+ # 1. Have project field matching
80
+ # 2. Have run_id that belongs to the project
70
81
  if project:
71
- assets = [a for a in assets if a.get("project") == project]
82
+ filtered_assets = []
83
+ for a in assets:
84
+ if a.get("project") == project:
85
+ filtered_assets.append(a)
86
+ elif a.get("run_id") in project_run_ids:
87
+ # Also include and tag with project
88
+ a["project"] = project
89
+ filtered_assets.append(a)
90
+ assets = filtered_assets
72
91
 
73
92
  assets = assets[:limit]
74
93
 
@@ -0,0 +1,96 @@
1
+ from fastapi import APIRouter, HTTPException, status, Response, Request
2
+ from pydantic import BaseModel
3
+ import os
4
+
5
+ router = APIRouter(prefix="/auth", tags=["auth"])
6
+
7
+
8
+ # --- Models ---
9
+ class User(BaseModel):
10
+ username: str
11
+ role: str = "admin"
12
+
13
+
14
+ class Token(BaseModel):
15
+ access_token: str
16
+ token_type: str
17
+
18
+
19
+ class LoginRequest(BaseModel):
20
+ username: str
21
+ password: str
22
+
23
+
24
+ # --- Config ---
25
+ # For simplicity in this iteration, we use single-user admin auth
26
+ ADMIN_USER = os.getenv("FLOWYML_ADMIN_USER", "admin")
27
+ ADMIN_PASSWORD = os.getenv("FLOWYML_ADMIN_PASSWORD", "flowyml")
28
+ # In production, this token is also a long-lived secret or signed JWT.
29
+ # For simplicity, we reuse the API Token logic or generate a session token.
30
+ # Here, we'll implement a simple session token mechanism or reuse API token.
31
+ API_TOKEN = os.getenv("FLOWYML_API_TOKEN")
32
+
33
+ # --- Routes ---
34
+
35
+
36
+ @router.post("/login", response_model=Token)
37
+ async def login(response: Response, login_data: LoginRequest):
38
+ """
39
+ Login with username and password.
40
+ Sets HttpOnly cookie for browser sessions.
41
+ Returns token for CLI/API use.
42
+ """
43
+ if login_data.username != ADMIN_USER or login_data.password != ADMIN_PASSWORD:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_401_UNAUTHORIZED,
46
+ detail="Incorrect username or password",
47
+ headers={"WWW-Authenticate": "Bearer"},
48
+ )
49
+
50
+ # Use the API token as the session token for simplicity in this unified auth model
51
+ # Ensure API_TOKEN is set in production!
52
+ if not API_TOKEN:
53
+ # Fallback for dev if not set (though middleware skips auth in dev usually)
54
+ token = "dev-token-placeholder" # noqa: S105
55
+ else:
56
+ token = API_TOKEN
57
+
58
+ # Set HttpOnly cookie
59
+ # accessible only by server, secure in prod
60
+ response.set_cookie(
61
+ key="access_token",
62
+ value=f"Bearer {token}",
63
+ httponly=True,
64
+ max_age=86400 * 7, # 7 days
65
+ secure=os.getenv("FLOWYML_ENV") == "production",
66
+ samesite="lax",
67
+ )
68
+
69
+ return {"access_token": token, "token_type": "bearer"}
70
+
71
+
72
+ @router.post("/logout")
73
+ async def logout(response: Response):
74
+ """Clear session cookie."""
75
+ response.delete_cookie("access_token")
76
+ return {"message": "Logged out successfully"}
77
+
78
+
79
+ @router.get("/me", response_model=User)
80
+ async def get_current_user(request: Request):
81
+ """
82
+ Get current logged in user.
83
+ Used by frontend to verify session.
84
+ """
85
+ # If middleware let us through, we are authenticated.
86
+ # In 'production' mode, unauth requests are blocked by middleware.
87
+ # In 'development' mode, middleware skips, so we check env.
88
+
89
+ if os.getenv("FLOWYML_ENV") != "production":
90
+ # Local dev: always return default admin
91
+ return {"username": "developer", "role": "admin"}
92
+
93
+ # In production, if we reached here, AuthMiddleware validated us.
94
+ # We can inspect headers/cookies to determine *who* it is
95
+ # (if we had multi-user), but for now it's just Admin.
96
+ return {"username": ADMIN_USER, "role": "admin"}