mcli-framework 7.5.1__py3-none-any.whl → 7.6.1__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/commands_cmd.py +51 -39
- mcli/app/completion_helpers.py +4 -13
- mcli/app/main.py +21 -25
- mcli/app/model_cmd.py +119 -9
- mcli/lib/custom_commands.py +16 -11
- mcli/ml/api/app.py +1 -5
- mcli/ml/dashboard/app.py +2 -2
- mcli/ml/dashboard/app_integrated.py +168 -116
- mcli/ml/dashboard/app_supabase.py +7 -3
- mcli/ml/dashboard/app_training.py +3 -6
- mcli/ml/dashboard/components/charts.py +74 -115
- mcli/ml/dashboard/components/metrics.py +24 -44
- mcli/ml/dashboard/components/tables.py +32 -40
- mcli/ml/dashboard/overview.py +102 -78
- mcli/ml/dashboard/pages/cicd.py +103 -56
- mcli/ml/dashboard/pages/debug_dependencies.py +35 -28
- mcli/ml/dashboard/pages/gravity_viz.py +374 -313
- mcli/ml/dashboard/pages/monte_carlo_predictions.py +50 -48
- mcli/ml/dashboard/pages/predictions_enhanced.py +396 -248
- mcli/ml/dashboard/pages/scrapers_and_logs.py +299 -273
- mcli/ml/dashboard/pages/test_portfolio.py +153 -121
- mcli/ml/dashboard/pages/trading.py +238 -169
- mcli/ml/dashboard/pages/workflows.py +129 -84
- mcli/ml/dashboard/streamlit_extras_utils.py +70 -79
- mcli/ml/dashboard/utils.py +24 -21
- mcli/ml/dashboard/warning_suppression.py +6 -4
- mcli/ml/database/session.py +16 -5
- mcli/ml/mlops/pipeline_orchestrator.py +1 -3
- mcli/ml/predictions/monte_carlo.py +6 -18
- mcli/ml/trading/alpaca_client.py +95 -96
- mcli/ml/trading/migrations.py +76 -40
- mcli/ml/trading/models.py +78 -60
- mcli/ml/trading/paper_trading.py +92 -74
- mcli/ml/trading/risk_management.py +106 -85
- mcli/ml/trading/trading_service.py +155 -110
- mcli/ml/training/train_model.py +1 -3
- mcli/{app → self}/completion_cmd.py +6 -6
- mcli/self/self_cmd.py +100 -57
- mcli/test/test_cmd.py +30 -0
- mcli/workflow/daemon/daemon.py +2 -0
- mcli/workflow/model_service/openai_adapter.py +347 -0
- mcli/workflow/politician_trading/models.py +6 -2
- mcli/workflow/politician_trading/scrapers_corporate_registry.py +39 -88
- mcli/workflow/politician_trading/scrapers_free_sources.py +32 -39
- mcli/workflow/politician_trading/scrapers_third_party.py +21 -39
- mcli/workflow/politician_trading/seed_database.py +70 -89
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/METADATA +1 -1
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/RECORD +56 -54
- /mcli/{app → self}/logs_cmd.py +0 -0
- /mcli/{app → self}/redis_cmd.py +0 -0
- /mcli/{app → self}/visual_cmd.py +0 -0
- /mcli/{app → test}/cron_test_cmd.py +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/WHEEL +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/entry_points.txt +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/licenses/LICENSE +0 -0
- {mcli_framework-7.5.1.dist-info → mcli_framework-7.6.1.dist-info}/top_level.txt +0 -0
mcli/app/commands_cmd.py
CHANGED
|
@@ -290,7 +290,9 @@ def {name}_command(name: str = "World"):
|
|
|
290
290
|
return template
|
|
291
291
|
|
|
292
292
|
|
|
293
|
-
def open_editor_for_command(
|
|
293
|
+
def open_editor_for_command(
|
|
294
|
+
command_name: str, command_group: str, description: str
|
|
295
|
+
) -> Optional[str]:
|
|
294
296
|
"""
|
|
295
297
|
Open the user's default editor to allow them to write command logic.
|
|
296
298
|
|
|
@@ -306,16 +308,18 @@ def open_editor_for_command(command_name: str, command_group: str, description:
|
|
|
306
308
|
import sys
|
|
307
309
|
|
|
308
310
|
# Get the user's default editor
|
|
309
|
-
editor = os.environ.get(
|
|
311
|
+
editor = os.environ.get("EDITOR")
|
|
310
312
|
if not editor:
|
|
311
313
|
# Try common editors in order of preference
|
|
312
|
-
for common_editor in [
|
|
313
|
-
if subprocess.run([
|
|
314
|
+
for common_editor in ["vim", "nano", "code", "subl", "atom", "emacs"]:
|
|
315
|
+
if subprocess.run(["which", common_editor], capture_output=True).returncode == 0:
|
|
314
316
|
editor = common_editor
|
|
315
317
|
break
|
|
316
318
|
|
|
317
319
|
if not editor:
|
|
318
|
-
click.echo(
|
|
320
|
+
click.echo(
|
|
321
|
+
"No editor found. Please set the EDITOR environment variable or install vim/nano."
|
|
322
|
+
)
|
|
319
323
|
return None
|
|
320
324
|
|
|
321
325
|
# Create a temporary file with the template
|
|
@@ -337,7 +341,7 @@ Example Click command structure:
|
|
|
337
341
|
@click.command()
|
|
338
342
|
@click.argument('name', default='World')
|
|
339
343
|
def my_command(name):
|
|
340
|
-
|
|
344
|
+
"""My custom command."""
|
|
341
345
|
click.echo(f"Hello, {{name}}!")
|
|
342
346
|
"""
|
|
343
347
|
import click
|
|
@@ -363,14 +367,16 @@ logger = get_logger()
|
|
|
363
367
|
'''
|
|
364
368
|
|
|
365
369
|
# Create temporary file
|
|
366
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
370
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
367
371
|
temp_file.write(enhanced_template)
|
|
368
372
|
temp_file_path = temp_file.name
|
|
369
373
|
|
|
370
374
|
try:
|
|
371
375
|
# Check if we're in an interactive environment
|
|
372
376
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
373
|
-
click.echo(
|
|
377
|
+
click.echo(
|
|
378
|
+
"Editor requires an interactive terminal. Use --template flag for non-interactive mode."
|
|
379
|
+
)
|
|
374
380
|
return None
|
|
375
381
|
|
|
376
382
|
# Open editor
|
|
@@ -386,7 +392,7 @@ logger = get_logger()
|
|
|
386
392
|
return None
|
|
387
393
|
|
|
388
394
|
# Read the edited content
|
|
389
|
-
with open(temp_file_path,
|
|
395
|
+
with open(temp_file_path, "r") as f:
|
|
390
396
|
edited_code = f.read()
|
|
391
397
|
|
|
392
398
|
# Check if the file was actually edited (not just the template)
|
|
@@ -395,12 +401,12 @@ logger = get_logger()
|
|
|
395
401
|
return None
|
|
396
402
|
|
|
397
403
|
# Extract the actual command code (remove the instructions)
|
|
398
|
-
lines = edited_code.split(
|
|
404
|
+
lines = edited_code.split("\n")
|
|
399
405
|
code_lines = []
|
|
400
406
|
in_code_section = False
|
|
401
407
|
|
|
402
408
|
for line in lines:
|
|
403
|
-
if line.strip().startswith(
|
|
409
|
+
if line.strip().startswith("# Your command implementation goes here:"):
|
|
404
410
|
in_code_section = True
|
|
405
411
|
continue
|
|
406
412
|
if in_code_section:
|
|
@@ -410,7 +416,7 @@ logger = get_logger()
|
|
|
410
416
|
# Fallback: use the entire file content
|
|
411
417
|
code_lines = lines
|
|
412
418
|
|
|
413
|
-
final_code =
|
|
419
|
+
final_code = "\n".join(code_lines).strip()
|
|
414
420
|
|
|
415
421
|
if not final_code:
|
|
416
422
|
click.echo("No command code found. Command creation cancelled.")
|
|
@@ -436,11 +442,12 @@ logger = get_logger()
|
|
|
436
442
|
@commands.command("add")
|
|
437
443
|
@click.argument("command_name", required=True)
|
|
438
444
|
@click.option("--group", "-g", help="Command group (defaults to 'workflow')", default="workflow")
|
|
445
|
+
@click.option("--description", "-d", help="Description for the command", default="Custom command")
|
|
439
446
|
@click.option(
|
|
440
|
-
"--
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
447
|
+
"--template",
|
|
448
|
+
"-t",
|
|
449
|
+
is_flag=True,
|
|
450
|
+
help="Use template mode (skip editor and use predefined template)",
|
|
444
451
|
)
|
|
445
452
|
def add_command(command_name, group, description, template):
|
|
446
453
|
"""
|
|
@@ -734,13 +741,13 @@ def edit_command(command_name, editor):
|
|
|
734
741
|
return 1
|
|
735
742
|
|
|
736
743
|
try:
|
|
737
|
-
with open(command_file,
|
|
744
|
+
with open(command_file, "r") as f:
|
|
738
745
|
command_data = json.load(f)
|
|
739
746
|
except Exception as e:
|
|
740
747
|
console.print(f"[red]Failed to load command: {e}[/red]")
|
|
741
748
|
return 1
|
|
742
749
|
|
|
743
|
-
code = command_data.get(
|
|
750
|
+
code = command_data.get("code", "")
|
|
744
751
|
|
|
745
752
|
if not code:
|
|
746
753
|
console.print(f"[red]Command has no code: {command_name}[/red]")
|
|
@@ -748,13 +755,14 @@ def edit_command(command_name, editor):
|
|
|
748
755
|
|
|
749
756
|
# Determine editor
|
|
750
757
|
if not editor:
|
|
751
|
-
editor = os.environ.get(
|
|
758
|
+
editor = os.environ.get("EDITOR", "vim")
|
|
752
759
|
|
|
753
760
|
console.print(f"Opening command in {editor}...")
|
|
754
761
|
|
|
755
762
|
# Create temp file with the code
|
|
756
|
-
with tempfile.NamedTemporaryFile(
|
|
757
|
-
|
|
763
|
+
with tempfile.NamedTemporaryFile(
|
|
764
|
+
mode="w", suffix=".py", delete=False, prefix=f"{command_name}_"
|
|
765
|
+
) as tmp:
|
|
758
766
|
tmp.write(code)
|
|
759
767
|
tmp_path = tmp.name
|
|
760
768
|
|
|
@@ -766,7 +774,7 @@ def edit_command(command_name, editor):
|
|
|
766
774
|
console.print(f"[yellow]Editor exited with code {result.returncode}[/yellow]")
|
|
767
775
|
|
|
768
776
|
# Read edited content
|
|
769
|
-
with open(tmp_path,
|
|
777
|
+
with open(tmp_path, "r") as f:
|
|
770
778
|
new_code = f.read()
|
|
771
779
|
|
|
772
780
|
# Check if code changed
|
|
@@ -776,20 +784,18 @@ def edit_command(command_name, editor):
|
|
|
776
784
|
|
|
777
785
|
# Validate syntax
|
|
778
786
|
try:
|
|
779
|
-
compile(new_code,
|
|
787
|
+
compile(new_code, "<string>", "exec")
|
|
780
788
|
except SyntaxError as e:
|
|
781
789
|
console.print(f"[red]Syntax error in edited code: {e}[/red]")
|
|
782
|
-
should_save = Prompt.ask(
|
|
783
|
-
"Save anyway?", choices=["y", "n"], default="n"
|
|
784
|
-
)
|
|
790
|
+
should_save = Prompt.ask("Save anyway?", choices=["y", "n"], default="n")
|
|
785
791
|
if should_save.lower() != "y":
|
|
786
792
|
return 1
|
|
787
793
|
|
|
788
794
|
# Update the command
|
|
789
|
-
command_data[
|
|
790
|
-
command_data[
|
|
795
|
+
command_data["code"] = new_code
|
|
796
|
+
command_data["updated_at"] = datetime.now().isoformat()
|
|
791
797
|
|
|
792
|
-
with open(command_file,
|
|
798
|
+
with open(command_file, "w") as f:
|
|
793
799
|
json.dump(command_data, f, indent=2)
|
|
794
800
|
|
|
795
801
|
# Update lockfile
|
|
@@ -832,7 +838,7 @@ def import_script(script_path, name, group, description, interactive):
|
|
|
832
838
|
|
|
833
839
|
# Read the script content
|
|
834
840
|
try:
|
|
835
|
-
with open(script_file,
|
|
841
|
+
with open(script_file, "r") as f:
|
|
836
842
|
code = f.read()
|
|
837
843
|
except Exception as e:
|
|
838
844
|
console.print(f"[red]Failed to read script: {e}[/red]")
|
|
@@ -849,11 +855,11 @@ def import_script(script_path, name, group, description, interactive):
|
|
|
849
855
|
|
|
850
856
|
# Interactive editing
|
|
851
857
|
if interactive:
|
|
852
|
-
editor = os.environ.get(
|
|
858
|
+
editor = os.environ.get("EDITOR", "vim")
|
|
853
859
|
console.print(f"Opening in {editor} for review...")
|
|
854
860
|
|
|
855
861
|
# Create temp file with the code
|
|
856
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
862
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as tmp:
|
|
857
863
|
tmp.write(code)
|
|
858
864
|
tmp_path = tmp.name
|
|
859
865
|
|
|
@@ -861,7 +867,7 @@ def import_script(script_path, name, group, description, interactive):
|
|
|
861
867
|
subprocess.run([editor, tmp_path], check=True)
|
|
862
868
|
|
|
863
869
|
# Read edited content
|
|
864
|
-
with open(tmp_path,
|
|
870
|
+
with open(tmp_path, "r") as f:
|
|
865
871
|
code = f.read()
|
|
866
872
|
finally:
|
|
867
873
|
Path(tmp_path).unlink(missing_ok=True)
|
|
@@ -870,6 +876,7 @@ def import_script(script_path, name, group, description, interactive):
|
|
|
870
876
|
if not description:
|
|
871
877
|
# Try to extract from docstring
|
|
872
878
|
import ast
|
|
879
|
+
|
|
873
880
|
try:
|
|
874
881
|
tree = ast.parse(code)
|
|
875
882
|
description = ast.get_docstring(tree) or f"Imported from {script_file.name}"
|
|
@@ -887,8 +894,8 @@ def import_script(script_path, name, group, description, interactive):
|
|
|
887
894
|
metadata={
|
|
888
895
|
"source": "import-script",
|
|
889
896
|
"original_file": str(script_file),
|
|
890
|
-
"imported_at": datetime.now().isoformat()
|
|
891
|
-
}
|
|
897
|
+
"imported_at": datetime.now().isoformat(),
|
|
898
|
+
},
|
|
892
899
|
)
|
|
893
900
|
|
|
894
901
|
console.print(f"[green]Imported script as command: {name}[/green]")
|
|
@@ -902,7 +909,12 @@ def import_script(script_path, name, group, description, interactive):
|
|
|
902
909
|
@commands.command("export-script")
|
|
903
910
|
@click.argument("command_name")
|
|
904
911
|
@click.option("--output", "-o", type=click.Path(), help="Output file path")
|
|
905
|
-
@click.option(
|
|
912
|
+
@click.option(
|
|
913
|
+
"--standalone",
|
|
914
|
+
"-s",
|
|
915
|
+
is_flag=True,
|
|
916
|
+
help="Make script standalone (add if __name__ == '__main__')",
|
|
917
|
+
)
|
|
906
918
|
def export_script(command_name, output, standalone):
|
|
907
919
|
"""
|
|
908
920
|
Export a JSON command to a Python script.
|
|
@@ -923,14 +935,14 @@ def export_script(command_name, output, standalone):
|
|
|
923
935
|
return 1
|
|
924
936
|
|
|
925
937
|
try:
|
|
926
|
-
with open(command_file,
|
|
938
|
+
with open(command_file, "r") as f:
|
|
927
939
|
command_data = json.load(f)
|
|
928
940
|
except Exception as e:
|
|
929
941
|
console.print(f"[red]Failed to load command: {e}[/red]")
|
|
930
942
|
return 1
|
|
931
943
|
|
|
932
944
|
# Get the code
|
|
933
|
-
code = command_data.get(
|
|
945
|
+
code = command_data.get("code", "")
|
|
934
946
|
|
|
935
947
|
if not code:
|
|
936
948
|
console.print(f"[red]Command has no code: {command_name}[/red]")
|
|
@@ -950,7 +962,7 @@ def export_script(command_name, output, standalone):
|
|
|
950
962
|
|
|
951
963
|
# Write the script
|
|
952
964
|
try:
|
|
953
|
-
with open(output_file,
|
|
965
|
+
with open(output_file, "w") as f:
|
|
954
966
|
f.write(code)
|
|
955
967
|
except Exception as e:
|
|
956
968
|
console.print(f"[red]Failed to write script: {e}[/red]")
|
mcli/app/completion_helpers.py
CHANGED
|
@@ -183,22 +183,13 @@ class CompletionAwareLazyGroup(click.Group):
|
|
|
183
183
|
group = self._load_group()
|
|
184
184
|
return group.list_commands(ctx)
|
|
185
185
|
|
|
186
|
-
def shell_complete(self, ctx, incomplete):
|
|
186
|
+
def shell_complete(self, ctx, param, incomplete):
|
|
187
187
|
"""Provide shell completion using static data when possible."""
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
data = LAZY_COMMAND_COMPLETIONS[self.name]
|
|
191
|
-
if "subcommands" in data:
|
|
192
|
-
items = []
|
|
193
|
-
for subcommand in data["subcommands"]:
|
|
194
|
-
if subcommand.startswith(incomplete):
|
|
195
|
-
items.append(CompletionItem(subcommand))
|
|
196
|
-
return items
|
|
197
|
-
|
|
198
|
-
# Fallback to loading the actual group
|
|
188
|
+
# Load the actual group to get proper completion for nested commands
|
|
189
|
+
# This ensures file path completion works for subcommands
|
|
199
190
|
group = self._load_group()
|
|
200
191
|
if hasattr(group, "shell_complete"):
|
|
201
|
-
return group.shell_complete(ctx, incomplete)
|
|
192
|
+
return group.shell_complete(ctx, param, incomplete)
|
|
202
193
|
return []
|
|
203
194
|
|
|
204
195
|
def get_params(self, ctx):
|
mcli/app/main.py
CHANGED
|
@@ -255,9 +255,15 @@ class LazyCommand(click.Command):
|
|
|
255
255
|
def shell_complete(self, ctx, param, incomplete):
|
|
256
256
|
"""Provide shell completion for the lazily loaded command."""
|
|
257
257
|
cmd = self._load_command()
|
|
258
|
+
# Delegate to the loaded command's completion
|
|
258
259
|
if hasattr(cmd, "shell_complete"):
|
|
259
260
|
return cmd.shell_complete(ctx, param, incomplete)
|
|
260
|
-
|
|
261
|
+
# Fallback to default Click completion
|
|
262
|
+
return (
|
|
263
|
+
super().shell_complete(ctx, param, incomplete)
|
|
264
|
+
if hasattr(super(), "shell_complete")
|
|
265
|
+
else []
|
|
266
|
+
)
|
|
261
267
|
|
|
262
268
|
|
|
263
269
|
class LazyGroup(click.Group):
|
|
@@ -309,9 +315,15 @@ class LazyGroup(click.Group):
|
|
|
309
315
|
def shell_complete(self, ctx, param, incomplete):
|
|
310
316
|
"""Provide shell completion for the lazily loaded group."""
|
|
311
317
|
group = self._load_group()
|
|
318
|
+
# Delegate to the loaded group's completion
|
|
312
319
|
if hasattr(group, "shell_complete"):
|
|
313
320
|
return group.shell_complete(ctx, param, incomplete)
|
|
314
|
-
|
|
321
|
+
# Fallback to default Click completion
|
|
322
|
+
return (
|
|
323
|
+
super().shell_complete(ctx, param, incomplete)
|
|
324
|
+
if hasattr(super(), "shell_complete")
|
|
325
|
+
else []
|
|
326
|
+
)
|
|
315
327
|
|
|
316
328
|
|
|
317
329
|
def _add_lazy_commands(app: click.Group):
|
|
@@ -334,14 +346,14 @@ def _add_lazy_commands(app: click.Group):
|
|
|
334
346
|
except Exception as e:
|
|
335
347
|
logger.debug(f"Could not load self commands: {e}")
|
|
336
348
|
|
|
337
|
-
#
|
|
349
|
+
# Test group - load immediately for testing commands
|
|
338
350
|
try:
|
|
339
|
-
from mcli.
|
|
351
|
+
from mcli.test.test_cmd import test_group
|
|
340
352
|
|
|
341
|
-
app.add_command(
|
|
342
|
-
logger.debug("Added
|
|
343
|
-
except
|
|
344
|
-
logger.debug(f"Could not load
|
|
353
|
+
app.add_command(test_group, name="test")
|
|
354
|
+
logger.debug("Added test group commands")
|
|
355
|
+
except Exception as e:
|
|
356
|
+
logger.debug(f"Could not load test commands: {e}")
|
|
345
357
|
|
|
346
358
|
# Add workflow with completion-aware lazy loading
|
|
347
359
|
try:
|
|
@@ -374,22 +386,6 @@ def _add_lazy_commands(app: click.Group):
|
|
|
374
386
|
"import_path": "mcli.app.model_cmd.model",
|
|
375
387
|
"help": "Model management commands for offline and online model usage",
|
|
376
388
|
},
|
|
377
|
-
"cron-test": {
|
|
378
|
-
"import_path": "mcli.app.cron_test_cmd.cron_test",
|
|
379
|
-
"help": "🕒 Validate and test MCLI cron/scheduler functionality with comprehensive tests.",
|
|
380
|
-
},
|
|
381
|
-
"visual": {
|
|
382
|
-
"import_path": "mcli.app.visual_cmd.visual",
|
|
383
|
-
"help": "🎨 Visual effects and enhancements showcase",
|
|
384
|
-
},
|
|
385
|
-
"redis": {
|
|
386
|
-
"import_path": "mcli.app.redis_cmd.redis_group",
|
|
387
|
-
"help": "🗄️ Manage Redis cache service for performance optimization",
|
|
388
|
-
},
|
|
389
|
-
"logs": {
|
|
390
|
-
"import_path": "mcli.app.logs_cmd.logs_group",
|
|
391
|
-
"help": "📋 Stream and manage MCLI log files with real-time updates",
|
|
392
|
-
},
|
|
393
389
|
}
|
|
394
390
|
|
|
395
391
|
for cmd_name, cmd_info in lazy_commands.items():
|
|
@@ -397,7 +393,7 @@ def _add_lazy_commands(app: click.Group):
|
|
|
397
393
|
if cmd_name == "workflow":
|
|
398
394
|
continue
|
|
399
395
|
|
|
400
|
-
if cmd_name in ["model"
|
|
396
|
+
if cmd_name in ["model"]:
|
|
401
397
|
# Use completion-aware LazyGroup for commands that have subcommands
|
|
402
398
|
try:
|
|
403
399
|
from mcli.app.completion_helpers import create_completion_aware_lazy_group
|
mcli/app/model_cmd.py
CHANGED
|
@@ -18,6 +18,86 @@ from mcli.workflow.model_service.lightweight_model_server import (
|
|
|
18
18
|
logger = get_logger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _start_openai_server(server, host: str, port: int, api_key: Optional[str], model: str):
|
|
22
|
+
"""Start FastAPI server with OpenAI compatibility"""
|
|
23
|
+
try:
|
|
24
|
+
import uvicorn
|
|
25
|
+
from fastapi import FastAPI
|
|
26
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
27
|
+
|
|
28
|
+
from mcli.workflow.model_service.openai_adapter import create_openai_adapter
|
|
29
|
+
|
|
30
|
+
# Create FastAPI app
|
|
31
|
+
app = FastAPI(
|
|
32
|
+
title="MCLI Model Service (OpenAI Compatible)",
|
|
33
|
+
description="OpenAI-compatible API for MCLI lightweight models",
|
|
34
|
+
version="1.0.0",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Add CORS middleware
|
|
38
|
+
app.add_middleware(
|
|
39
|
+
CORSMiddleware,
|
|
40
|
+
allow_origins=["*"],
|
|
41
|
+
allow_credentials=True,
|
|
42
|
+
allow_methods=["*"],
|
|
43
|
+
allow_headers=["*"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Create OpenAI adapter
|
|
47
|
+
require_auth = api_key is not None
|
|
48
|
+
adapter = create_openai_adapter(server, require_auth=require_auth)
|
|
49
|
+
|
|
50
|
+
# Add API key if provided
|
|
51
|
+
if api_key:
|
|
52
|
+
adapter.api_key_manager.add_key(api_key, name="default")
|
|
53
|
+
click.echo(f"🔐 API key authentication enabled")
|
|
54
|
+
|
|
55
|
+
# Include OpenAI routes
|
|
56
|
+
app.include_router(adapter.router)
|
|
57
|
+
|
|
58
|
+
# Add health check endpoint
|
|
59
|
+
@app.get("/health")
|
|
60
|
+
async def health():
|
|
61
|
+
return {"status": "healthy", "model": model}
|
|
62
|
+
|
|
63
|
+
# Display server info
|
|
64
|
+
click.echo(f"\n📝 Server running at:")
|
|
65
|
+
click.echo(f" - Base URL: http://{host}:{port}")
|
|
66
|
+
click.echo(f" - OpenAI API: http://{host}:{port}/v1")
|
|
67
|
+
click.echo(f" - Models: http://{host}:{port}/v1/models")
|
|
68
|
+
click.echo(f" - Chat: http://{host}:{port}/v1/chat/completions")
|
|
69
|
+
click.echo(f" - Health: http://{host}:{port}/health")
|
|
70
|
+
|
|
71
|
+
if require_auth:
|
|
72
|
+
click.echo(f"\n🔐 Authentication: Required")
|
|
73
|
+
click.echo(f" Use: Authorization: Bearer {api_key}")
|
|
74
|
+
else:
|
|
75
|
+
click.echo(f"\n⚠️ Authentication: Disabled (not recommended for public access)")
|
|
76
|
+
|
|
77
|
+
if host == "0.0.0.0":
|
|
78
|
+
click.echo(f"\n⚠️ Server is publicly accessible on all interfaces!")
|
|
79
|
+
|
|
80
|
+
click.echo(f"\n📚 For aider, use:")
|
|
81
|
+
if require_auth:
|
|
82
|
+
click.echo(f" export OPENAI_API_KEY={api_key}")
|
|
83
|
+
click.echo(f" export OPENAI_API_BASE=http://{host}:{port}/v1")
|
|
84
|
+
click.echo(f" aider --model {model}")
|
|
85
|
+
|
|
86
|
+
click.echo(f"\n Press Ctrl+C to stop the server")
|
|
87
|
+
|
|
88
|
+
# Start server
|
|
89
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
90
|
+
|
|
91
|
+
except ImportError as e:
|
|
92
|
+
click.echo(f"❌ Missing dependencies for OpenAI-compatible server: {e}")
|
|
93
|
+
click.echo(f" Install with: pip install fastapi uvicorn")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
click.echo(f"❌ Failed to start OpenAI-compatible server: {e}")
|
|
97
|
+
logger.error(f"Server error: {e}", exc_info=True)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
21
101
|
@click.group()
|
|
22
102
|
def model():
|
|
23
103
|
"""Model management commands for offline and online model usage."""
|
|
@@ -103,13 +183,34 @@ def download(model_name: str):
|
|
|
103
183
|
@click.option(
|
|
104
184
|
"--port", "-p", default=None, help="Port to run server on (default: from config or 51234)"
|
|
105
185
|
)
|
|
186
|
+
@click.option(
|
|
187
|
+
"--host", "-h", default="localhost", help="Host to bind to (use 0.0.0.0 for public access)"
|
|
188
|
+
)
|
|
106
189
|
@click.option(
|
|
107
190
|
"--auto-download",
|
|
108
191
|
is_flag=True,
|
|
109
192
|
default=True,
|
|
110
193
|
help="Automatically download model if not available",
|
|
111
194
|
)
|
|
112
|
-
|
|
195
|
+
@click.option(
|
|
196
|
+
"--openai-compatible",
|
|
197
|
+
is_flag=True,
|
|
198
|
+
default=False,
|
|
199
|
+
help="Enable OpenAI-compatible API endpoints",
|
|
200
|
+
)
|
|
201
|
+
@click.option(
|
|
202
|
+
"--api-key",
|
|
203
|
+
default=None,
|
|
204
|
+
help="API key for authentication (if not set, auth is disabled)",
|
|
205
|
+
)
|
|
206
|
+
def start(
|
|
207
|
+
model: Optional[str],
|
|
208
|
+
port: Optional[int],
|
|
209
|
+
host: str,
|
|
210
|
+
auto_download: bool,
|
|
211
|
+
openai_compatible: bool,
|
|
212
|
+
api_key: Optional[str],
|
|
213
|
+
):
|
|
113
214
|
"""Start the lightweight model server."""
|
|
114
215
|
# Load port from config if not specified
|
|
115
216
|
if port is None:
|
|
@@ -155,15 +256,24 @@ def start(model: Optional[str], port: Optional[int], auto_download: bool):
|
|
|
155
256
|
click.echo(f"❌ Failed to load {model}")
|
|
156
257
|
sys.exit(1)
|
|
157
258
|
|
|
158
|
-
# Start server
|
|
159
|
-
|
|
160
|
-
|
|
259
|
+
# Start server with OpenAI compatibility if requested
|
|
260
|
+
if openai_compatible:
|
|
261
|
+
click.echo(f"🚀 Starting OpenAI-compatible server on {host}:{port}...")
|
|
262
|
+
_start_openai_server(server, host, port, api_key, model)
|
|
263
|
+
else:
|
|
264
|
+
click.echo(f"🚀 Starting lightweight server on {host}:{port}...")
|
|
265
|
+
server.start_server()
|
|
266
|
+
|
|
267
|
+
click.echo(f"\n📝 Server running at:")
|
|
268
|
+
click.echo(f" - API: http://{host}:{port}")
|
|
269
|
+
click.echo(f" - Health: http://{host}:{port}/health")
|
|
270
|
+
click.echo(f" - Models: http://{host}:{port}/models")
|
|
271
|
+
|
|
272
|
+
if host == "0.0.0.0":
|
|
273
|
+
click.echo(f"\n⚠️ Server is publicly accessible!")
|
|
274
|
+
click.echo(f" Consider using --openai-compatible with --api-key for security")
|
|
161
275
|
|
|
162
|
-
|
|
163
|
-
click.echo(f" - API: http://localhost:{port}")
|
|
164
|
-
click.echo(f" - Health: http://localhost:{port}/health")
|
|
165
|
-
click.echo(f" - Models: http://localhost:{port}/models")
|
|
166
|
-
click.echo(f"\n Press Ctrl+C to stop the server")
|
|
276
|
+
click.echo(f"\n Press Ctrl+C to stop the server")
|
|
167
277
|
|
|
168
278
|
try:
|
|
169
279
|
# Keep server running
|
mcli/lib/custom_commands.py
CHANGED
|
@@ -5,8 +5,8 @@ This module provides functionality to store user-created commands in a portable
|
|
|
5
5
|
format in ~/.mcli/commands/ and automatically load them at startup.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import json
|
|
9
8
|
import importlib.util
|
|
9
|
+
import json
|
|
10
10
|
import sys
|
|
11
11
|
import tempfile
|
|
12
12
|
from datetime import datetime
|
|
@@ -259,9 +259,7 @@ class CustomCommandManager:
|
|
|
259
259
|
module_name = f"mcli_custom_{name}"
|
|
260
260
|
|
|
261
261
|
# Create a temporary file to store the code
|
|
262
|
-
with tempfile.NamedTemporaryFile(
|
|
263
|
-
mode="w", suffix=".py", delete=False
|
|
264
|
-
) as temp_file:
|
|
262
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as temp_file:
|
|
265
263
|
temp_file.write(code)
|
|
266
264
|
temp_file_path = temp_file.name
|
|
267
265
|
|
|
@@ -274,12 +272,23 @@ class CustomCommandManager:
|
|
|
274
272
|
spec.loader.exec_module(module)
|
|
275
273
|
|
|
276
274
|
# Look for a command or command group in the module
|
|
275
|
+
# Prioritize Groups over Commands to handle commands with subcommands correctly
|
|
277
276
|
command_obj = None
|
|
277
|
+
found_commands = []
|
|
278
|
+
|
|
278
279
|
for attr_name in dir(module):
|
|
279
280
|
attr = getattr(module, attr_name)
|
|
280
|
-
if isinstance(attr,
|
|
281
|
+
if isinstance(attr, click.Group):
|
|
282
|
+
# Found a group - this takes priority
|
|
281
283
|
command_obj = attr
|
|
282
284
|
break
|
|
285
|
+
elif isinstance(attr, click.Command):
|
|
286
|
+
# Store command for fallback
|
|
287
|
+
found_commands.append(attr)
|
|
288
|
+
|
|
289
|
+
# If no group found, use the first command
|
|
290
|
+
if not command_obj and found_commands:
|
|
291
|
+
command_obj = found_commands[0]
|
|
283
292
|
|
|
284
293
|
if command_obj:
|
|
285
294
|
# Register with the target group
|
|
@@ -288,9 +297,7 @@ class CustomCommandManager:
|
|
|
288
297
|
logger.info(f"Registered custom command: {name}")
|
|
289
298
|
return True
|
|
290
299
|
else:
|
|
291
|
-
logger.warning(
|
|
292
|
-
f"No Click command found in custom command: {name}"
|
|
293
|
-
)
|
|
300
|
+
logger.warning(f"No Click command found in custom command: {name}")
|
|
294
301
|
return False
|
|
295
302
|
finally:
|
|
296
303
|
# Clean up temporary file
|
|
@@ -320,9 +327,7 @@ class CustomCommandManager:
|
|
|
320
327
|
logger.error(f"Failed to export commands: {e}")
|
|
321
328
|
return False
|
|
322
329
|
|
|
323
|
-
def import_commands(
|
|
324
|
-
self, import_path: Path, overwrite: bool = False
|
|
325
|
-
) -> Dict[str, bool]:
|
|
330
|
+
def import_commands(self, import_path: Path, overwrite: bool = False) -> Dict[str, bool]:
|
|
326
331
|
"""
|
|
327
332
|
Import commands from a JSON file.
|
|
328
333
|
|
mcli/ml/api/app.py
CHANGED
|
@@ -16,11 +16,7 @@ from mcli.ml.config import settings
|
|
|
16
16
|
from mcli.ml.database.session import init_db
|
|
17
17
|
from mcli.ml.logging import get_logger, setup_logging
|
|
18
18
|
|
|
19
|
-
from .middleware import
|
|
20
|
-
ErrorHandlingMiddleware,
|
|
21
|
-
RateLimitMiddleware,
|
|
22
|
-
RequestLoggingMiddleware,
|
|
23
|
-
)
|
|
19
|
+
from .middleware import ErrorHandlingMiddleware, RateLimitMiddleware, RequestLoggingMiddleware
|
|
24
20
|
from .routers import (
|
|
25
21
|
admin_router,
|
|
26
22
|
auth_router,
|
mcli/ml/dashboard/app.py
CHANGED
|
@@ -14,6 +14,8 @@ from plotly.subplots import make_subplots
|
|
|
14
14
|
|
|
15
15
|
from mcli.ml.cache import cache_manager
|
|
16
16
|
from mcli.ml.config import settings
|
|
17
|
+
from mcli.ml.dashboard.common import setup_page_config
|
|
18
|
+
from mcli.ml.dashboard.styles import apply_dashboard_styles
|
|
17
19
|
from mcli.ml.database.models import (
|
|
18
20
|
BacktestResult,
|
|
19
21
|
Model,
|
|
@@ -25,8 +27,6 @@ from mcli.ml.database.models import (
|
|
|
25
27
|
User,
|
|
26
28
|
)
|
|
27
29
|
from mcli.ml.database.session import SessionLocal
|
|
28
|
-
from mcli.ml.dashboard.common import setup_page_config
|
|
29
|
-
from mcli.ml.dashboard.styles import apply_dashboard_styles
|
|
30
30
|
|
|
31
31
|
# Page config - must be first
|
|
32
32
|
setup_page_config(page_title="MCLI ML Dashboard")
|