mcli-framework 7.0.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.
Potentially problematic release.
This version of mcli-framework might be problematic. Click here for more details.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
mcli/cli.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from mcli.lib.api.mcli_decorators import chat
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@click.group()
|
|
7
|
+
def cli():
|
|
8
|
+
"""MCLI - Modern Command Line Interface"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Add the chat command group
|
|
13
|
+
cli.add_command(chat(name="chat"))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@cli.command()
|
|
17
|
+
def version():
|
|
18
|
+
"""Show MCLI version"""
|
|
19
|
+
from mcli import __version__
|
|
20
|
+
|
|
21
|
+
click.echo(f"MCLI version {__version__}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
cli()
|
mcli/config.toml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[paths]
|
|
2
|
+
included_dirs = ["app", "self", "workflow", "public"]
|
|
3
|
+
|
|
4
|
+
# Chat configuration with lightweight local models by default
|
|
5
|
+
[llm]
|
|
6
|
+
provider = "local"
|
|
7
|
+
model = "prajjwal1/bert-tiny"
|
|
8
|
+
temperature = 0.7
|
|
9
|
+
system_prompt = "You are the MCLI Chat Assistant, an expert AI assistant for the MCLI command line tool.\n\nCRITICAL RULES:\n- NEVER suggest commands that don't exist\n- ONLY reference commands from the provided available commands list\n- If functionality doesn't exist, clearly state it needs to be created\n- ALWAYS be accurate about what commands are available\n\nYou can:\n1. **Create new commands**: Generate Python code using Click framework when functionality is missing\n2. **Execute existing commands**: Help users run commands that actually exist\n3. **Explain functionality**: Provide accurate explanations based on real commands\n4. **Integrate external code**: Help create new commands from GitHub repositories\n5. **File operations**: Generate new commands for missing functionality\n\nWhen creating commands:\n- Use Click framework (@click.command() decorator)\n- Include proper help text and argument descriptions\n- Add error handling and validation\n- Follow Python best practices\n- Provide specific file paths and testing instructions\n\nBe accurate, helpful, and never hallucinate non-existent commands."
|
|
10
|
+
# Default to lightweight model server (offline mode)
|
|
11
|
+
ollama_base_url = "http://localhost:8080"
|
|
12
|
+
# Set to true to use lightweight models by default (can override with --remote flag)
|
|
13
|
+
use_lightweight_models = true
|
|
14
|
+
|
|
15
|
+
# Alternative providers (uncomment to use)
|
|
16
|
+
# provider = "openai"
|
|
17
|
+
# model = "gpt-4-turbo"
|
|
18
|
+
# openai_api_key = "your-api-key-here"
|
|
19
|
+
|
|
20
|
+
anthropic_api_key = "your-api-key-here"
|
mcli/lib/api/api.py
ADDED
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import socket
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
import requests
|
|
16
|
+
import uvicorn
|
|
17
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
18
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
|
|
21
|
+
# Import existing utilities
|
|
22
|
+
from mcli.lib.logger.logger import get_logger
|
|
23
|
+
from mcli.lib.toml.toml import read_from_toml
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
|
|
27
|
+
# Global FastAPI app instance
|
|
28
|
+
_api_app: Optional[FastAPI] = None
|
|
29
|
+
_api_server_thread: Optional[threading.Thread] = None
|
|
30
|
+
_api_server_running = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def find_free_port(start_port: int = 8000, max_attempts: int = 100) -> int:
|
|
34
|
+
"""
|
|
35
|
+
Find a free port starting from start_port.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
start_port: Starting port number
|
|
39
|
+
max_attempts: Maximum number of attempts to find a free port
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A free port number
|
|
43
|
+
"""
|
|
44
|
+
for attempt in range(max_attempts):
|
|
45
|
+
port = start_port + attempt
|
|
46
|
+
try:
|
|
47
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
48
|
+
s.bind(("", port))
|
|
49
|
+
return port
|
|
50
|
+
except OSError:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# If no free port found, use a random port in a safe range
|
|
54
|
+
return random.randint(49152, 65535)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_api_config() -> Dict[str, Any]:
|
|
58
|
+
"""
|
|
59
|
+
Get API configuration from MCLI config files.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dictionary with API configuration
|
|
63
|
+
"""
|
|
64
|
+
config = {
|
|
65
|
+
"enabled": False,
|
|
66
|
+
"host": "0.0.0.0",
|
|
67
|
+
"port": None, # Will be set to random port if None
|
|
68
|
+
"use_random_port": True,
|
|
69
|
+
"debug": False,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Try to load from config.toml files
|
|
73
|
+
config_paths = [
|
|
74
|
+
Path("config.toml"), # Current directory
|
|
75
|
+
Path.home() / ".config" / "mcli" / "config.toml", # User config
|
|
76
|
+
Path(__file__).parent.parent.parent.parent.parent / "config.toml", # Project root
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
for config_path in config_paths:
|
|
80
|
+
if config_path.exists():
|
|
81
|
+
try:
|
|
82
|
+
api_config = read_from_toml(str(config_path), "api")
|
|
83
|
+
if api_config:
|
|
84
|
+
config.update(api_config)
|
|
85
|
+
logger.debug(f"Loaded API config from {config_path}")
|
|
86
|
+
break
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.debug(f"Could not load API config from {config_path}: {e}")
|
|
89
|
+
|
|
90
|
+
# Override with environment variables
|
|
91
|
+
if os.environ.get("MCLI_API_SERVER", "false").lower() in ("true", "1", "yes"):
|
|
92
|
+
config["enabled"] = True
|
|
93
|
+
|
|
94
|
+
if os.environ.get("MCLI_API_HOST"):
|
|
95
|
+
config["host"] = os.environ.get("MCLI_API_HOST")
|
|
96
|
+
|
|
97
|
+
if os.environ.get("MCLI_API_PORT"):
|
|
98
|
+
config["port"] = int(os.environ.get("MCLI_API_PORT"))
|
|
99
|
+
config["use_random_port"] = False
|
|
100
|
+
|
|
101
|
+
if os.environ.get("MCLI_API_DEBUG", "false").lower() in ("true", "1", "yes"):
|
|
102
|
+
config["debug"] = True
|
|
103
|
+
|
|
104
|
+
# Set random port if needed
|
|
105
|
+
if config["enabled"] and config["use_random_port"] and config["port"] is None:
|
|
106
|
+
config["port"] = find_free_port()
|
|
107
|
+
logger.info(f"Using random port: {config['port']}")
|
|
108
|
+
|
|
109
|
+
return config
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ClickToAPIDecorator:
|
|
113
|
+
"""Decorator that makes Click commands also serve as API endpoints."""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
endpoint_path: str = None,
|
|
118
|
+
http_method: str = "POST",
|
|
119
|
+
response_model: BaseModel = None,
|
|
120
|
+
description: str = None,
|
|
121
|
+
tags: List[str] = None,
|
|
122
|
+
):
|
|
123
|
+
"""
|
|
124
|
+
Initialize the decorator.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
endpoint_path: API endpoint path (defaults to command name)
|
|
128
|
+
http_method: HTTP method (GET, POST, PUT, DELETE)
|
|
129
|
+
response_model: Pydantic model for response validation
|
|
130
|
+
description: API endpoint description
|
|
131
|
+
tags: API tags for grouping
|
|
132
|
+
"""
|
|
133
|
+
self.endpoint_path = endpoint_path
|
|
134
|
+
self.http_method = http_method.upper()
|
|
135
|
+
self.response_model = response_model
|
|
136
|
+
self.description = description
|
|
137
|
+
self.tags = tags or []
|
|
138
|
+
|
|
139
|
+
def __call__(self, func: Callable) -> Callable:
|
|
140
|
+
"""Apply the decorator to a function."""
|
|
141
|
+
|
|
142
|
+
# Get the original function signature
|
|
143
|
+
sig = inspect.signature(func)
|
|
144
|
+
|
|
145
|
+
# Create a wrapper that registers the API endpoint
|
|
146
|
+
@functools.wraps(func)
|
|
147
|
+
def wrapper(*args, **kwargs):
|
|
148
|
+
# Register the API endpoint
|
|
149
|
+
self._register_api_endpoint(func, sig)
|
|
150
|
+
|
|
151
|
+
# Call the original function
|
|
152
|
+
return func(*args, **kwargs)
|
|
153
|
+
|
|
154
|
+
# Store decorator info for later registration
|
|
155
|
+
wrapper._api_decorator = self
|
|
156
|
+
wrapper._original_func = func
|
|
157
|
+
|
|
158
|
+
return wrapper
|
|
159
|
+
|
|
160
|
+
def _register_api_endpoint(self, func: Callable, sig: inspect.Signature):
|
|
161
|
+
"""Register the function as an API endpoint."""
|
|
162
|
+
global _api_app
|
|
163
|
+
|
|
164
|
+
# Ensure API app exists
|
|
165
|
+
if _api_app is None:
|
|
166
|
+
_api_app = ensure_api_app()
|
|
167
|
+
if _api_app is None:
|
|
168
|
+
logger.warning("Could not create API app, skipping endpoint registration")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
# Determine endpoint path
|
|
172
|
+
endpoint_path = self.endpoint_path or f"/{func.__name__}"
|
|
173
|
+
|
|
174
|
+
# Create request model from function signature
|
|
175
|
+
request_model = self._create_request_model(func, sig)
|
|
176
|
+
|
|
177
|
+
# Create response model
|
|
178
|
+
response_model = self.response_model or self._create_response_model()
|
|
179
|
+
|
|
180
|
+
# Register the endpoint
|
|
181
|
+
self._register_endpoint(
|
|
182
|
+
app=_api_app,
|
|
183
|
+
path=endpoint_path,
|
|
184
|
+
method=self.http_method,
|
|
185
|
+
func=func,
|
|
186
|
+
request_model=request_model,
|
|
187
|
+
response_model=response_model,
|
|
188
|
+
description=self.description or func.__doc__,
|
|
189
|
+
tags=self.tags,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
logger.info(f"Registered API endpoint: {self.http_method} {endpoint_path}")
|
|
193
|
+
|
|
194
|
+
def _create_fastapi_app(self) -> FastAPI:
|
|
195
|
+
"""Create and configure FastAPI app."""
|
|
196
|
+
app = FastAPI(
|
|
197
|
+
title="MCLI API", description="API endpoints for MCLI commands", version="1.0.0"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Add CORS middleware
|
|
201
|
+
app.add_middleware(
|
|
202
|
+
CORSMiddleware,
|
|
203
|
+
allow_origins=["*"],
|
|
204
|
+
allow_credentials=True,
|
|
205
|
+
allow_methods=["*"],
|
|
206
|
+
allow_headers=["*"],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Add health check endpoint
|
|
210
|
+
@app.get("/health")
|
|
211
|
+
async def health_check():
|
|
212
|
+
return {"status": "healthy", "service": "MCLI API"}
|
|
213
|
+
|
|
214
|
+
# Add root endpoint
|
|
215
|
+
@app.get("/")
|
|
216
|
+
async def root():
|
|
217
|
+
return {
|
|
218
|
+
"service": "MCLI API",
|
|
219
|
+
"version": "1.0.0",
|
|
220
|
+
"status": "running",
|
|
221
|
+
"endpoints": self._get_registered_endpoints(app),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return app
|
|
225
|
+
|
|
226
|
+
def _create_request_model(self, func: Callable, sig: inspect.Signature) -> BaseModel:
|
|
227
|
+
"""Create a Pydantic model from function signature."""
|
|
228
|
+
fields = {}
|
|
229
|
+
|
|
230
|
+
for param_name, param in sig.parameters.items():
|
|
231
|
+
if param_name in ["self", "ctx"]:
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
# Get parameter type and default
|
|
235
|
+
param_type = param.annotation if param.annotation != inspect.Parameter.empty else str
|
|
236
|
+
default = param.default if param.default != inspect.Parameter.empty else ...
|
|
237
|
+
|
|
238
|
+
# Handle Click-specific types
|
|
239
|
+
if hasattr(param_type, "__origin__") and param_type.__origin__ is Union:
|
|
240
|
+
# Handle Union types (e.g., Optional[str])
|
|
241
|
+
param_type = str
|
|
242
|
+
elif param_type == bool:
|
|
243
|
+
# Handle boolean flags
|
|
244
|
+
param_type = bool
|
|
245
|
+
elif param_type in [int, float]:
|
|
246
|
+
param_type = param_type
|
|
247
|
+
else:
|
|
248
|
+
param_type = str
|
|
249
|
+
|
|
250
|
+
fields[param_name] = (param_type, default)
|
|
251
|
+
|
|
252
|
+
# Create dynamic model
|
|
253
|
+
model_name = f"{func.__name__}Request"
|
|
254
|
+
return type(model_name, (BaseModel,), fields)
|
|
255
|
+
|
|
256
|
+
def _create_response_model(self) -> BaseModel:
|
|
257
|
+
"""Create a default response model."""
|
|
258
|
+
|
|
259
|
+
class DefaultResponse(BaseModel):
|
|
260
|
+
success: bool = Field(..., description="Operation success status")
|
|
261
|
+
result: Any = Field(None, description="Operation result")
|
|
262
|
+
message: str = Field("", description="Operation message")
|
|
263
|
+
error: str = Field(None, description="Error message if any")
|
|
264
|
+
|
|
265
|
+
return DefaultResponse
|
|
266
|
+
|
|
267
|
+
def _register_endpoint(
|
|
268
|
+
self,
|
|
269
|
+
app: FastAPI,
|
|
270
|
+
path: str,
|
|
271
|
+
method: str,
|
|
272
|
+
func: Callable,
|
|
273
|
+
request_model: BaseModel,
|
|
274
|
+
response_model: BaseModel,
|
|
275
|
+
description: str,
|
|
276
|
+
tags: List[str],
|
|
277
|
+
):
|
|
278
|
+
"""Register an endpoint with FastAPI."""
|
|
279
|
+
|
|
280
|
+
async def api_endpoint(request: request_model):
|
|
281
|
+
"""API endpoint wrapper."""
|
|
282
|
+
try:
|
|
283
|
+
# Convert request model to kwargs
|
|
284
|
+
kwargs = request.dict()
|
|
285
|
+
|
|
286
|
+
# Call the original function
|
|
287
|
+
result = func(**kwargs)
|
|
288
|
+
|
|
289
|
+
# Return response
|
|
290
|
+
return response_model(
|
|
291
|
+
success=True, result=result, message="Operation completed successfully"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
except Exception as e:
|
|
295
|
+
logger.error(f"API endpoint error: {e}")
|
|
296
|
+
return response_model(success=False, error=str(e), message="Operation failed")
|
|
297
|
+
|
|
298
|
+
# Register with FastAPI
|
|
299
|
+
if method == "GET":
|
|
300
|
+
app.get(path, response_model=response_model, description=description, tags=tags)(
|
|
301
|
+
api_endpoint
|
|
302
|
+
)
|
|
303
|
+
elif method == "POST":
|
|
304
|
+
app.post(path, response_model=response_model, description=description, tags=tags)(
|
|
305
|
+
api_endpoint
|
|
306
|
+
)
|
|
307
|
+
elif method == "PUT":
|
|
308
|
+
app.put(path, response_model=response_model, description=description, tags=tags)(
|
|
309
|
+
api_endpoint
|
|
310
|
+
)
|
|
311
|
+
elif method == "DELETE":
|
|
312
|
+
app.delete(path, response_model=response_model, description=description, tags=tags)(
|
|
313
|
+
api_endpoint
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _get_registered_endpoints(self, app: FastAPI) -> List[Dict[str, str]]:
|
|
317
|
+
"""Get list of registered endpoints."""
|
|
318
|
+
endpoints = []
|
|
319
|
+
for route in app.routes:
|
|
320
|
+
if hasattr(route, "path") and hasattr(route, "methods"):
|
|
321
|
+
for method in route.methods:
|
|
322
|
+
endpoints.append(
|
|
323
|
+
{
|
|
324
|
+
"path": route.path,
|
|
325
|
+
"method": method,
|
|
326
|
+
"name": getattr(route, "name", "Unknown"),
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
return endpoints
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def api_endpoint(
|
|
333
|
+
endpoint_path: str = None,
|
|
334
|
+
http_method: str = "POST",
|
|
335
|
+
response_model: BaseModel = None,
|
|
336
|
+
description: str = None,
|
|
337
|
+
tags: List[str] = None,
|
|
338
|
+
):
|
|
339
|
+
"""
|
|
340
|
+
Decorator that makes Click commands also serve as API endpoints.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
endpoint_path: API endpoint path (defaults to command name)
|
|
344
|
+
http_method: HTTP method (GET, POST, PUT, DELETE)
|
|
345
|
+
response_model: Pydantic model for response validation
|
|
346
|
+
description: API endpoint description
|
|
347
|
+
tags: API tags for grouping
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
@click.command()
|
|
351
|
+
@api_endpoint("/generate", "POST")
|
|
352
|
+
def generate_text(prompt: str, max_length: int = 100):
|
|
353
|
+
return {"text": "Generated text"}
|
|
354
|
+
"""
|
|
355
|
+
return ClickToAPIDecorator(
|
|
356
|
+
endpoint_path=endpoint_path,
|
|
357
|
+
http_method=http_method,
|
|
358
|
+
response_model=response_model,
|
|
359
|
+
description=description,
|
|
360
|
+
tags=tags,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def ensure_api_app() -> Optional[FastAPI]:
|
|
365
|
+
"""Ensure the API app is created and return it."""
|
|
366
|
+
global _api_app
|
|
367
|
+
|
|
368
|
+
if _api_app is None:
|
|
369
|
+
# Get configuration
|
|
370
|
+
config = get_api_config()
|
|
371
|
+
|
|
372
|
+
# Check if API server should be enabled
|
|
373
|
+
if not config["enabled"]:
|
|
374
|
+
logger.debug("API server is disabled in configuration")
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
# Create the API app
|
|
378
|
+
_api_app = FastAPI(
|
|
379
|
+
title="MCLI API", description="API endpoints for MCLI commands", version="1.0.0"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Add CORS middleware
|
|
383
|
+
_api_app.add_middleware(
|
|
384
|
+
CORSMiddleware,
|
|
385
|
+
allow_origins=["*"],
|
|
386
|
+
allow_credentials=True,
|
|
387
|
+
allow_methods=["*"],
|
|
388
|
+
allow_headers=["*"],
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Add health check endpoint
|
|
392
|
+
@_api_app.get("/health")
|
|
393
|
+
async def health_check():
|
|
394
|
+
return {"status": "healthy", "service": "MCLI API"}
|
|
395
|
+
|
|
396
|
+
# Add root endpoint
|
|
397
|
+
@_api_app.get("/")
|
|
398
|
+
async def root():
|
|
399
|
+
return {
|
|
400
|
+
"service": "MCLI API",
|
|
401
|
+
"version": "1.0.0",
|
|
402
|
+
"status": "running",
|
|
403
|
+
"config": {
|
|
404
|
+
"host": config.get("host", "0.0.0.0"),
|
|
405
|
+
"port": config.get("port", "random"),
|
|
406
|
+
"debug": config.get("debug", False),
|
|
407
|
+
},
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
logger.debug("API app created successfully")
|
|
411
|
+
|
|
412
|
+
return _api_app
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def start_api_server(host: str = None, port: int = None, debug: bool = None) -> str:
|
|
416
|
+
"""Start the API server with configuration from MCLI config."""
|
|
417
|
+
global _api_app, _api_server_thread, _api_server_running
|
|
418
|
+
|
|
419
|
+
# Get configuration
|
|
420
|
+
config = get_api_config()
|
|
421
|
+
|
|
422
|
+
# Override with parameters if provided
|
|
423
|
+
if host is not None:
|
|
424
|
+
config["host"] = host
|
|
425
|
+
if port is not None:
|
|
426
|
+
config["port"] = port
|
|
427
|
+
config["use_random_port"] = False
|
|
428
|
+
if debug is not None:
|
|
429
|
+
config["debug"] = debug
|
|
430
|
+
|
|
431
|
+
# Check if API server should be enabled
|
|
432
|
+
if not config["enabled"]:
|
|
433
|
+
logger.info("API server is disabled in configuration")
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
# Find port if not specified
|
|
437
|
+
if config["port"] is None:
|
|
438
|
+
config["port"] = find_free_port()
|
|
439
|
+
logger.info(f"Using random port: {config['port']}")
|
|
440
|
+
|
|
441
|
+
if _api_app is None:
|
|
442
|
+
_api_app = FastAPI(
|
|
443
|
+
title="MCLI API", description="API endpoints for MCLI commands", version="1.0.0"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Add CORS middleware
|
|
447
|
+
_api_app.add_middleware(
|
|
448
|
+
CORSMiddleware,
|
|
449
|
+
allow_origins=["*"],
|
|
450
|
+
allow_credentials=True,
|
|
451
|
+
allow_methods=["*"],
|
|
452
|
+
allow_headers=["*"],
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Add health check endpoint
|
|
456
|
+
@_api_app.get("/health")
|
|
457
|
+
async def health_check():
|
|
458
|
+
return {"status": "healthy", "service": "MCLI API"}
|
|
459
|
+
|
|
460
|
+
# Add root endpoint
|
|
461
|
+
@_api_app.get("/")
|
|
462
|
+
async def root():
|
|
463
|
+
return {
|
|
464
|
+
"service": "MCLI API",
|
|
465
|
+
"version": "1.0.0",
|
|
466
|
+
"status": "running",
|
|
467
|
+
"config": {
|
|
468
|
+
"host": config["host"],
|
|
469
|
+
"port": config["port"],
|
|
470
|
+
"debug": config["debug"],
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
def run_server():
|
|
475
|
+
uvicorn.run(
|
|
476
|
+
_api_app, host=config["host"], port=config["port"], log_level="error", access_log=False
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if not _api_server_running:
|
|
480
|
+
_api_server_thread = threading.Thread(target=run_server, daemon=True)
|
|
481
|
+
_api_server_thread.start()
|
|
482
|
+
_api_server_running = True
|
|
483
|
+
|
|
484
|
+
# Wait a moment for server to start
|
|
485
|
+
time.sleep(1)
|
|
486
|
+
|
|
487
|
+
api_url = f"http://{config['host']}:{config['port']}"
|
|
488
|
+
logger.info(f"API server started at {api_url}")
|
|
489
|
+
logger.info(f"Health check: {api_url}/health")
|
|
490
|
+
logger.info(f"Documentation: {api_url}/docs")
|
|
491
|
+
|
|
492
|
+
return api_url
|
|
493
|
+
|
|
494
|
+
return f"http://{config['host']}:{config['port']}"
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def stop_api_server():
|
|
498
|
+
"""Stop the API server."""
|
|
499
|
+
global _api_server_running
|
|
500
|
+
|
|
501
|
+
if _api_server_running:
|
|
502
|
+
# In a real implementation, you'd want to properly shutdown the server
|
|
503
|
+
# For now, we'll just set the flag
|
|
504
|
+
_api_server_running = False
|
|
505
|
+
logger.info("API server stopped")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def get_api_app() -> Optional[FastAPI]:
|
|
509
|
+
"""Get the FastAPI app instance."""
|
|
510
|
+
return _api_app
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def is_api_server_running() -> bool:
|
|
514
|
+
"""Check if the API server is running."""
|
|
515
|
+
return _api_server_running
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def register_command_as_api(
|
|
519
|
+
command_func: Callable,
|
|
520
|
+
endpoint_path: str = None,
|
|
521
|
+
http_method: str = "POST",
|
|
522
|
+
response_model: BaseModel = None,
|
|
523
|
+
description: str = None,
|
|
524
|
+
tags: List[str] = None,
|
|
525
|
+
):
|
|
526
|
+
"""
|
|
527
|
+
Register a Click command as an API endpoint.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
command_func: The Click command function
|
|
531
|
+
endpoint_path: API endpoint path
|
|
532
|
+
http_method: HTTP method
|
|
533
|
+
response_model: Pydantic model for response
|
|
534
|
+
description: API endpoint description
|
|
535
|
+
tags: API tags for grouping
|
|
536
|
+
"""
|
|
537
|
+
logger.info(
|
|
538
|
+
f"register_command_as_api called for: {command_func.__name__} with path: {endpoint_path}"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Ensure API app is created
|
|
542
|
+
api_app = ensure_api_app()
|
|
543
|
+
if api_app is None:
|
|
544
|
+
logger.debug("API app not available, skipping endpoint registration")
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
logger.info(f"API app available, proceeding with registration")
|
|
548
|
+
|
|
549
|
+
decorator = ClickToAPIDecorator(
|
|
550
|
+
endpoint_path=endpoint_path,
|
|
551
|
+
http_method=http_method,
|
|
552
|
+
response_model=response_model,
|
|
553
|
+
description=description,
|
|
554
|
+
tags=tags,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Register the endpoint directly with the API app
|
|
558
|
+
sig = inspect.signature(command_func)
|
|
559
|
+
decorator._register_api_endpoint(command_func, sig)
|
|
560
|
+
|
|
561
|
+
logger.info(
|
|
562
|
+
f"Registered command as API endpoint: {http_method} {endpoint_path or f'/{command_func.__name__}'}"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# Convenience function for common response models
|
|
567
|
+
def create_success_response_model(result_type: type = str):
|
|
568
|
+
"""Create a success response model."""
|
|
569
|
+
|
|
570
|
+
class SuccessResponse(BaseModel):
|
|
571
|
+
success: bool = Field(True, description="Operation success status")
|
|
572
|
+
result: result_type = Field(..., description="Operation result")
|
|
573
|
+
message: str = Field("Operation completed successfully", description="Operation message")
|
|
574
|
+
|
|
575
|
+
return SuccessResponse
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def create_error_response_model():
|
|
579
|
+
"""Create an error response model."""
|
|
580
|
+
|
|
581
|
+
class ErrorResponse(BaseModel):
|
|
582
|
+
success: bool = Field(False, description="Operation success status")
|
|
583
|
+
error: str = Field(..., description="Error message")
|
|
584
|
+
message: str = Field("Operation failed", description="Operation message")
|
|
585
|
+
|
|
586
|
+
return ErrorResponse
|