remdb 0.3.14__py3-none-any.whl → 0.3.133__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 (89) hide show
  1. rem/agentic/README.md +76 -0
  2. rem/agentic/__init__.py +15 -0
  3. rem/agentic/agents/__init__.py +16 -2
  4. rem/agentic/agents/sse_simulator.py +502 -0
  5. rem/agentic/context.py +51 -27
  6. rem/agentic/llm_provider_models.py +301 -0
  7. rem/agentic/mcp/tool_wrapper.py +112 -17
  8. rem/agentic/otel/setup.py +93 -4
  9. rem/agentic/providers/phoenix.py +302 -109
  10. rem/agentic/providers/pydantic_ai.py +215 -26
  11. rem/agentic/schema.py +361 -21
  12. rem/agentic/tools/rem_tools.py +3 -3
  13. rem/api/README.md +215 -1
  14. rem/api/deps.py +255 -0
  15. rem/api/main.py +132 -40
  16. rem/api/mcp_router/resources.py +1 -1
  17. rem/api/mcp_router/server.py +26 -5
  18. rem/api/mcp_router/tools.py +465 -7
  19. rem/api/routers/admin.py +494 -0
  20. rem/api/routers/auth.py +70 -0
  21. rem/api/routers/chat/completions.py +402 -20
  22. rem/api/routers/chat/models.py +88 -10
  23. rem/api/routers/chat/otel_utils.py +33 -0
  24. rem/api/routers/chat/sse_events.py +542 -0
  25. rem/api/routers/chat/streaming.py +642 -45
  26. rem/api/routers/dev.py +81 -0
  27. rem/api/routers/feedback.py +268 -0
  28. rem/api/routers/messages.py +473 -0
  29. rem/api/routers/models.py +78 -0
  30. rem/api/routers/query.py +360 -0
  31. rem/api/routers/shared_sessions.py +406 -0
  32. rem/auth/middleware.py +126 -27
  33. rem/cli/commands/README.md +237 -64
  34. rem/cli/commands/cluster.py +1808 -0
  35. rem/cli/commands/configure.py +1 -3
  36. rem/cli/commands/db.py +386 -143
  37. rem/cli/commands/experiments.py +418 -27
  38. rem/cli/commands/process.py +14 -8
  39. rem/cli/commands/schema.py +97 -50
  40. rem/cli/main.py +27 -6
  41. rem/config.py +10 -3
  42. rem/models/core/core_model.py +7 -1
  43. rem/models/core/experiment.py +54 -0
  44. rem/models/core/rem_query.py +5 -2
  45. rem/models/entities/__init__.py +21 -0
  46. rem/models/entities/domain_resource.py +38 -0
  47. rem/models/entities/feedback.py +123 -0
  48. rem/models/entities/message.py +30 -1
  49. rem/models/entities/session.py +83 -0
  50. rem/models/entities/shared_session.py +180 -0
  51. rem/registry.py +10 -4
  52. rem/schemas/agents/rem.yaml +7 -3
  53. rem/services/content/service.py +92 -20
  54. rem/services/embeddings/api.py +4 -4
  55. rem/services/embeddings/worker.py +16 -16
  56. rem/services/phoenix/client.py +154 -14
  57. rem/services/postgres/README.md +159 -15
  58. rem/services/postgres/__init__.py +2 -1
  59. rem/services/postgres/diff_service.py +531 -0
  60. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  61. rem/services/postgres/repository.py +132 -0
  62. rem/services/postgres/schema_generator.py +205 -4
  63. rem/services/postgres/service.py +6 -6
  64. rem/services/rem/parser.py +44 -9
  65. rem/services/rem/service.py +36 -2
  66. rem/services/session/compression.py +24 -1
  67. rem/services/session/reload.py +1 -1
  68. rem/settings.py +324 -23
  69. rem/sql/background_indexes.sql +21 -16
  70. rem/sql/migrations/001_install.sql +387 -54
  71. rem/sql/migrations/002_install_models.sql +2320 -393
  72. rem/sql/migrations/003_optional_extensions.sql +326 -0
  73. rem/sql/migrations/004_cache_system.sql +548 -0
  74. rem/utils/__init__.py +18 -0
  75. rem/utils/date_utils.py +2 -2
  76. rem/utils/model_helpers.py +156 -1
  77. rem/utils/schema_loader.py +220 -22
  78. rem/utils/sql_paths.py +146 -0
  79. rem/utils/sql_types.py +3 -1
  80. rem/workers/__init__.py +3 -1
  81. rem/workers/db_listener.py +579 -0
  82. rem/workers/unlogged_maintainer.py +463 -0
  83. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/METADATA +335 -226
  84. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/RECORD +86 -66
  85. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/WHEEL +1 -1
  86. rem/sql/002_install_models.sql +0 -1068
  87. rem/sql/install_models.sql +0 -1051
  88. rem/sql/migrations/003_seed_default_user.sql +0 -48
  89. {remdb-0.3.14.dist-info → remdb-0.3.133.dist-info}/entry_points.txt +0 -0
@@ -63,6 +63,7 @@ def experiments():
63
63
  @experiments.command("create")
64
64
  @click.argument("name")
65
65
  @click.option("--agent", "-a", required=True, help="Agent schema name (e.g., 'cv-parser')")
66
+ @click.option("--task", "-t", default="general", help="Task name for organizing experiments (e.g., 'risk-assessment')")
66
67
  @click.option("--evaluator", "-e", default="default", help="Evaluator schema name (default: 'default')")
67
68
  @click.option("--description", "-d", help="Experiment description")
68
69
  @click.option("--dataset-location", type=click.Choice(["git", "s3", "hybrid"]), default="git",
@@ -74,6 +75,7 @@ def experiments():
74
75
  def create(
75
76
  name: str,
76
77
  agent: str,
78
+ task: str,
77
79
  evaluator: str,
78
80
  description: Optional[str],
79
81
  dataset_location: str,
@@ -170,7 +172,8 @@ def create(
170
172
  # Create experiment config
171
173
  config = ExperimentConfig(
172
174
  name=name,
173
- description=description or f"Evaluation experiment for {agent} agent",
175
+ task=task,
176
+ description=description or f"Evaluation experiment for {agent} agent ({task} task)",
174
177
  agent_schema_ref=SchemaReference(
175
178
  name=agent,
176
179
  version=None, # Use latest by default
@@ -514,6 +517,159 @@ def show(name: str, base_path: Optional[str]):
514
517
  raise click.Abort()
515
518
 
516
519
 
520
+ # =============================================================================
521
+ # VIBES MODE HELPER
522
+ # =============================================================================
523
+
524
+
525
+ def _run_vibes_mode(
526
+ config: Any,
527
+ dataset_df: Any,
528
+ task_fn: Any,
529
+ base_path: str,
530
+ limit: Optional[int],
531
+ evaluator_schema_path: Path,
532
+ ) -> None:
533
+ """Run experiment in vibes mode - execute agent and export for AI evaluation.
534
+
535
+ Vibes mode runs the agent on each example and saves results to a JSONL file.
536
+ The AI assistant (e.g., Claude Code) then acts as the judge using the
537
+ evaluator schema to evaluate results.
538
+
539
+ Args:
540
+ config: ExperimentConfig object
541
+ dataset_df: Polars DataFrame with ground truth examples
542
+ task_fn: Function to run agent on each example
543
+ base_path: Base directory for experiments
544
+ limit: Optional limit on number of examples to process
545
+ evaluator_schema_path: Path to the evaluator schema YAML file
546
+ """
547
+ from rem.utils.date_utils import format_timestamp_for_experiment, utc_now, to_iso
548
+ import json
549
+
550
+ # Apply limit if specified
551
+ if limit:
552
+ dataset_df = dataset_df.head(limit)
553
+ click.echo(f" (Limited to {limit} examples)")
554
+
555
+ # Create results directory
556
+ timestamp = format_timestamp_for_experiment()
557
+ results_dir = Path(base_path) / config.name / "results" / timestamp
558
+ results_dir.mkdir(parents=True, exist_ok=True)
559
+
560
+ click.echo(f"\n⏳ Running agent on {len(dataset_df)} examples...")
561
+ click.echo(f" Results will be saved to: {results_dir}")
562
+ click.echo()
563
+
564
+ # Run agent on each example and collect results
565
+ results = []
566
+ records = dataset_df.to_dicts()
567
+
568
+ for i, record in enumerate(records, 1):
569
+ example_id = record.get("id", i)
570
+ click.echo(f" [{i}/{len(records)}] Processing example {example_id}...", nl=False)
571
+
572
+ try:
573
+ # Prepare input for agent
574
+ input_text = record.get("text", record.get("input", record.get("query", "")))
575
+ example_input = {"query": input_text} if isinstance(input_text, str) else input_text
576
+
577
+ # Run agent
578
+ output = task_fn({"input": example_input})
579
+
580
+ result = {
581
+ "id": example_id,
582
+ "input": input_text,
583
+ "ground_truth": record.get("ground_truth", record.get("expected_output", "")),
584
+ "category": record.get("category", ""),
585
+ "agent_output": output,
586
+ "status": "success",
587
+ }
588
+ click.echo(" ✓")
589
+
590
+ except Exception as e:
591
+ result = {
592
+ "id": example_id,
593
+ "input": record.get("text", record.get("input", "")),
594
+ "ground_truth": record.get("ground_truth", record.get("expected_output", "")),
595
+ "category": record.get("category", ""),
596
+ "agent_output": None,
597
+ "status": "error",
598
+ "error": str(e),
599
+ }
600
+ click.echo(f" ✗ ({e})")
601
+
602
+ results.append(result)
603
+
604
+ # Save results to JSONL
605
+ results_file = results_dir / "vibes-results.jsonl"
606
+ with open(results_file, "w") as f:
607
+ for result in results:
608
+ f.write(json.dumps(result) + "\n")
609
+
610
+ # Copy evaluator schema to results dir for easy reference
611
+ import shutil
612
+ evaluator_copy = results_dir / "evaluator-schema.yaml"
613
+ shutil.copy(evaluator_schema_path, evaluator_copy)
614
+
615
+ # Save run metadata
616
+ run_info = {
617
+ "experiment": config.name,
618
+ "agent": config.agent_schema_ref.name,
619
+ "evaluator": config.evaluator_schema_ref.name,
620
+ "mode": "vibes",
621
+ "timestamp": timestamp,
622
+ "total_examples": len(records),
623
+ "successful": len([r for r in results if r["status"] == "success"]),
624
+ "failed": len([r for r in results if r["status"] == "error"]),
625
+ "completed_at": to_iso(utc_now()),
626
+ }
627
+
628
+ run_info_file = results_dir / "run-info.json"
629
+ with open(run_info_file, "w") as f:
630
+ json.dump(run_info, f, indent=2)
631
+
632
+ # Print summary and instructions
633
+ success_count = run_info["successful"]
634
+ fail_count = run_info["failed"]
635
+
636
+ click.echo(f"\n{'=' * 60}")
637
+ click.echo(f"VIBES MODE COMPLETE")
638
+ click.echo(f"{'=' * 60}")
639
+ click.echo(f"\nResults: {success_count} successful, {fail_count} failed")
640
+ click.echo(f"\nFiles saved to: {results_dir}/")
641
+ click.echo(f" - vibes-results.jsonl (agent outputs)")
642
+ click.echo(f" - evaluator-schema.yaml (evaluation criteria)")
643
+ click.echo(f" - run-info.json (run metadata)")
644
+
645
+ click.echo(f"\n{'=' * 60}")
646
+ click.echo(f"NEXT STEP: Ask your AI assistant to evaluate")
647
+ click.echo(f"{'=' * 60}")
648
+ click.echo(f"""
649
+ Copy this prompt to Claude Code or your AI assistant:
650
+
651
+ Please evaluate the experiment results in:
652
+ {results_dir}/
653
+
654
+ Read the vibes-results.jsonl file and evaluate each example
655
+ using the evaluator schema in evaluator-schema.yaml.
656
+
657
+ For each example, provide:
658
+ 1. extracted_classification
659
+ 2. exact_match (vs ground_truth)
660
+ 3. semantic_match
661
+ 4. reasoning_quality_score
662
+ 5. overall_score
663
+ 6. pass/fail
664
+
665
+ Then provide summary metrics:
666
+ - Exact match accuracy
667
+ - Semantic match accuracy
668
+ - Average overall score
669
+ - Pass rate
670
+ """)
671
+
672
+
517
673
  # =============================================================================
518
674
  # RUN COMMAND
519
675
  # =============================================================================
@@ -524,6 +680,8 @@ def show(name: str, base_path: Optional[str]):
524
680
  @click.option("--base-path", help="Base directory for experiments (default: EXPERIMENTS_HOME or 'experiments')")
525
681
  @click.option("--version", help="Git tag version to load (e.g., 'experiments/my-exp/v1.0.0')")
526
682
  @click.option("--dry-run", is_flag=True, help="Test on small subset without saving")
683
+ @click.option("--only-vibes", is_flag=True, help="Run agent locally, export results for AI evaluation (no Phoenix)")
684
+ @click.option("--limit", "-n", type=int, help="Limit number of examples to evaluate (useful with --only-vibes)")
527
685
  @click.option("--update-prompts", is_flag=True, help="Update prompts in Phoenix before running")
528
686
  @click.option("--phoenix-url", help="Phoenix server URL (overrides PHOENIX_BASE_URL env var)")
529
687
  @click.option("--phoenix-api-key", help="Phoenix API key (overrides PHOENIX_API_KEY env var)")
@@ -532,14 +690,45 @@ def run(
532
690
  base_path: Optional[str],
533
691
  version: Optional[str],
534
692
  dry_run: bool,
693
+ only_vibes: bool,
694
+ limit: Optional[int],
535
695
  update_prompts: bool,
536
696
  phoenix_url: Optional[str],
537
697
  phoenix_api_key: Optional[str],
538
698
  ):
539
- """Run an experiment using Phoenix provider.
699
+ """Run an experiment using Phoenix provider or local vibes mode.
540
700
 
541
701
  Loads configuration, executes agent and evaluator, saves results.
542
702
 
703
+ Vibes Mode (--only-vibes):
704
+ Run agent locally without Phoenix infrastructure. Agent outputs are saved
705
+ to a JSONL file along with the evaluator schema. Your AI assistant (e.g.,
706
+ Claude Code) then acts as the judge to evaluate results.
707
+
708
+ This enables seamless switching between:
709
+ - Local evaluation: Quick iteration with AI-as-judge
710
+ - Phoenix evaluation: Production metrics and dashboards
711
+
712
+ Usage:
713
+ rem experiments run my-experiment --only-vibes
714
+ rem experiments run my-experiment --only-vibes --limit 5
715
+
716
+ The command will:
717
+ 1. Run the agent on each ground-truth example
718
+ 2. Save results to results/{timestamp}/vibes-results.jsonl
719
+ 3. Print the evaluator prompt and schema
720
+ 4. Instruct you to ask your AI assistant to evaluate
721
+
722
+ Example workflow with Claude Code:
723
+ $ rem experiments run mental-health-classifier --only-vibes --limit 3
724
+ # ... agent runs ...
725
+ # Results saved to: .experiments/mental-health-classifier/results/20241203-143022/
726
+
727
+ # Then ask Claude Code:
728
+ "Please evaluate the experiment results in
729
+ .experiments/mental-health-classifier/results/20241203-143022/
730
+ using the evaluator schema provided"
731
+
543
732
  Phoenix Connection:
544
733
  Commands respect PHOENIX_BASE_URL and PHOENIX_API_KEY environment variables.
545
734
  Defaults to localhost:6006 for local development.
@@ -562,6 +751,12 @@ def run(
562
751
  # Run experiment with latest schemas
563
752
  rem experiments run hello-world-validation
564
753
 
754
+ # Quick local evaluation (vibes mode)
755
+ rem experiments run hello-world-validation --only-vibes
756
+
757
+ # Vibes mode with limited examples
758
+ rem experiments run hello-world-validation --only-vibes --limit 5
759
+
565
760
  # Run specific version
566
761
  rem experiments run hello-world-validation \\
567
762
  --version experiments/hello-world-validation/v1.0.0
@@ -674,35 +869,47 @@ def run(
674
869
 
675
870
  click.echo(f"Loading evaluator: {evaluator_name} for agent {agent_name}")
676
871
 
677
- # Try multiple evaluator path patterns (agent-specific, then generic)
678
- evaluator_paths_to_try = [
679
- f"{agent_name}/{evaluator_name}", # e.g., hello-world/default
680
- f"{agent_name}-{evaluator_name}", # e.g., hello-world-default
681
- evaluator_name, # e.g., default (generic)
682
- ]
872
+ # Find evaluator schema file path
873
+ from rem.utils.schema_loader import get_evaluator_schema_path
683
874
 
875
+ evaluator_schema_path = get_evaluator_schema_path(evaluator_name)
876
+ if not evaluator_schema_path or not evaluator_schema_path.exists():
877
+ click.echo(f"Error: Could not find evaluator schema '{evaluator_name}'")
878
+ raise click.Abort()
879
+
880
+ click.echo(f"✓ Found evaluator schema: {evaluator_schema_path}")
881
+
882
+ # For Phoenix mode, also load evaluator function
684
883
  evaluator_fn = None
685
- evaluator_load_error = None
884
+ if not only_vibes:
885
+ # Try multiple evaluator path patterns (agent-specific, then generic)
886
+ evaluator_paths_to_try = [
887
+ f"{agent_name}/{evaluator_name}", # e.g., hello-world/default
888
+ f"{agent_name}-{evaluator_name}", # e.g., hello-world-default
889
+ evaluator_name, # e.g., default (generic)
890
+ ]
686
891
 
687
- for evaluator_path in evaluator_paths_to_try:
688
- try:
689
- evaluator_fn = create_evaluator_from_schema(
690
- evaluator_schema_path=evaluator_path,
691
- model_name=None, # Use default from schema
692
- )
693
- click.echo(f"✓ Loaded evaluator schema: {evaluator_path}")
694
- break
695
- except FileNotFoundError as e:
696
- evaluator_load_error = e
697
- logger.debug(f"Evaluator not found at {evaluator_path}: {e}")
698
- continue
699
- except Exception as e:
700
- evaluator_load_error = e
701
- logger.warning(f"Failed to load evaluator from {evaluator_path}: {e}")
702
- continue
892
+ evaluator_load_error = None
893
+
894
+ for evaluator_path in evaluator_paths_to_try:
895
+ try:
896
+ evaluator_fn = create_evaluator_from_schema(
897
+ evaluator_schema_path=evaluator_path,
898
+ model_name=None, # Use default from schema
899
+ )
900
+ click.echo(f"✓ Loaded evaluator function: {evaluator_path}")
901
+ break
902
+ except FileNotFoundError as e:
903
+ evaluator_load_error = e
904
+ logger.debug(f"Evaluator not found at {evaluator_path}: {e}")
905
+ continue
906
+ except Exception as e:
907
+ evaluator_load_error = e
908
+ logger.warning(f"Failed to load evaluator from {evaluator_path}: {e}")
909
+ continue
703
910
 
704
- if evaluator_fn is None:
705
- click.echo(f"Error: Could not load evaluator schema '{evaluator_name}'")
911
+ if evaluator_fn is None and not only_vibes:
912
+ click.echo(f"Error: Could not load evaluator function '{evaluator_name}'")
706
913
  click.echo(f" Tried paths: {evaluator_paths_to_try}")
707
914
  if evaluator_load_error:
708
915
  click.echo(f" Last error: {evaluator_load_error}")
@@ -769,6 +976,18 @@ def run(
769
976
  # TODO: Implement prompt updating
770
977
  click.echo("⚠ --update-prompts not yet implemented")
771
978
 
979
+ # Vibes mode: run agent and export for AI evaluation
980
+ if only_vibes:
981
+ _run_vibes_mode(
982
+ config=config,
983
+ dataset_df=dataset_df,
984
+ task_fn=task_fn,
985
+ base_path=base_path,
986
+ limit=limit,
987
+ evaluator_schema_path=evaluator_schema_path,
988
+ )
989
+ return
990
+
772
991
  # Run experiment via Phoenix
773
992
  if not dry_run:
774
993
  # Create Phoenix client with optional overrides
@@ -1304,3 +1523,175 @@ def trace_list(
1304
1523
  logger.error(f"Failed to list traces: {e}")
1305
1524
  click.echo(f"Error: {e}", err=True)
1306
1525
  raise click.Abort()
1526
+
1527
+
1528
+ # =============================================================================
1529
+ # EXPORT COMMAND
1530
+ # =============================================================================
1531
+
1532
+
1533
+ @experiments.command("export")
1534
+ @click.argument("name")
1535
+ @click.option("--base-path", help="Base directory for experiments (default: EXPERIMENTS_HOME or 'experiments')")
1536
+ @click.option("--bucket", "-b", help="S3 bucket name (default: DATA_LAKE__BUCKET_NAME)")
1537
+ @click.option("--version", "-v", default="v0", help="Data lake version prefix (default: v0)")
1538
+ @click.option("--plan", is_flag=True, help="Show what would be exported without uploading")
1539
+ @click.option("--include-results", is_flag=True, help="Include results directory in export")
1540
+ def export(
1541
+ name: str,
1542
+ base_path: Optional[str],
1543
+ bucket: Optional[str],
1544
+ version: str,
1545
+ plan: bool,
1546
+ include_results: bool,
1547
+ ):
1548
+ """Export experiment to S3 data lake.
1549
+
1550
+ Exports experiment configuration, ground truth, and optionally results
1551
+ to the S3 data lake following the convention:
1552
+
1553
+ s3://{bucket}/{version}/datasets/calibration/experiments/{agent}/{task}/
1554
+
1555
+ The export includes:
1556
+ - experiment.yaml (configuration)
1557
+ - README.md (documentation)
1558
+ - ground-truth/ (evaluation datasets)
1559
+ - seed-data/ (optional seed data)
1560
+ - results/ (optional, with --include-results)
1561
+
1562
+ Examples:
1563
+ # Preview what would be exported
1564
+ rem experiments export my-experiment --plan
1565
+
1566
+ # Export to configured data lake bucket
1567
+ rem experiments export my-experiment
1568
+
1569
+ # Export to specific bucket
1570
+ rem experiments export my-experiment --bucket siggy-data
1571
+
1572
+ # Include results in export
1573
+ rem experiments export my-experiment --include-results
1574
+
1575
+ # Export with custom version prefix
1576
+ rem experiments export my-experiment --version v1
1577
+ """
1578
+ from rem.models.core.experiment import ExperimentConfig
1579
+ from rem.settings import settings
1580
+ from rem.services.fs.s3_provider import S3Provider
1581
+ import os
1582
+ import json
1583
+
1584
+ try:
1585
+ # Resolve base path
1586
+ if base_path is None:
1587
+ base_path = os.getenv("EXPERIMENTS_HOME", "experiments")
1588
+
1589
+ # Load experiment configuration
1590
+ config_path = Path(base_path) / name / "experiment.yaml"
1591
+ if not config_path.exists():
1592
+ click.echo(f"Experiment not found: {name}")
1593
+ click.echo(f" Looked in: {config_path}")
1594
+ raise click.Abort()
1595
+
1596
+ config = ExperimentConfig.from_yaml(config_path)
1597
+ click.echo(f"✓ Loaded experiment: {name}")
1598
+
1599
+ # Resolve bucket
1600
+ if bucket is None:
1601
+ bucket = settings.data_lake.bucket_name
1602
+ if bucket is None:
1603
+ click.echo("Error: No S3 bucket configured.")
1604
+ click.echo(" Set DATA_LAKE__BUCKET_NAME environment variable or use --bucket option")
1605
+ raise click.Abort()
1606
+
1607
+ # Build S3 paths
1608
+ s3_base = config.get_s3_export_path(bucket, version)
1609
+ exp_dir = config.get_experiment_dir(base_path)
1610
+
1611
+ # Collect files to export
1612
+ files_to_export = []
1613
+
1614
+ # Always include these files
1615
+ required_files = [
1616
+ ("experiment.yaml", exp_dir / "experiment.yaml"),
1617
+ ("README.md", exp_dir / "README.md"),
1618
+ ]
1619
+
1620
+ for s3_name, local_path in required_files:
1621
+ if local_path.exists():
1622
+ files_to_export.append((s3_name, local_path))
1623
+
1624
+ # Include ground-truth directory
1625
+ ground_truth_dir = exp_dir / "ground-truth"
1626
+ if ground_truth_dir.exists():
1627
+ for f in ground_truth_dir.rglob("*"):
1628
+ if f.is_file():
1629
+ relative = f.relative_to(exp_dir)
1630
+ files_to_export.append((str(relative), f))
1631
+
1632
+ # Include seed-data directory
1633
+ seed_data_dir = exp_dir / "seed-data"
1634
+ if seed_data_dir.exists():
1635
+ for f in seed_data_dir.rglob("*"):
1636
+ if f.is_file():
1637
+ relative = f.relative_to(exp_dir)
1638
+ files_to_export.append((str(relative), f))
1639
+
1640
+ # Optionally include results
1641
+ if include_results:
1642
+ results_dir = exp_dir / "results"
1643
+ if results_dir.exists():
1644
+ for f in results_dir.rglob("*"):
1645
+ if f.is_file():
1646
+ relative = f.relative_to(exp_dir)
1647
+ files_to_export.append((str(relative), f))
1648
+
1649
+ # Display export plan
1650
+ click.echo(f"\n{'=' * 60}")
1651
+ click.echo(f"EXPORT {'PLAN' if plan else 'TO S3'}")
1652
+ click.echo(f"{'=' * 60}")
1653
+ click.echo(f"\nExperiment: {config.name}")
1654
+ click.echo(f"Agent: {config.agent_schema_ref.name}")
1655
+ click.echo(f"Task: {config.task}")
1656
+ click.echo(f"Evaluator file: {config.get_evaluator_filename()}")
1657
+ click.echo(f"\nDestination: {s3_base}/")
1658
+ click.echo(f"\nFiles to export ({len(files_to_export)}):")
1659
+
1660
+ for s3_name, local_path in files_to_export:
1661
+ s3_uri = f"{s3_base}/{s3_name}"
1662
+ if plan:
1663
+ click.echo(f" {local_path}")
1664
+ click.echo(f" → {s3_uri}")
1665
+ else:
1666
+ click.echo(f" {s3_name}")
1667
+
1668
+ if plan:
1669
+ click.echo(f"\n[PLAN MODE] No files were uploaded.")
1670
+ click.echo(f"Run without --plan to execute the export.")
1671
+ return
1672
+
1673
+ # Execute export
1674
+ click.echo(f"\n⏳ Uploading to S3...")
1675
+ s3 = S3Provider()
1676
+
1677
+ uploaded = 0
1678
+ for s3_name, local_path in files_to_export:
1679
+ s3_uri = f"{s3_base}/{s3_name}"
1680
+ try:
1681
+ s3.copy(str(local_path), s3_uri)
1682
+ uploaded += 1
1683
+ click.echo(f" ✓ {s3_name}")
1684
+ except Exception as e:
1685
+ click.echo(f" ✗ {s3_name}: {e}")
1686
+
1687
+ click.echo(f"\n✓ Exported {uploaded}/{len(files_to_export)} files to {s3_base}/")
1688
+
1689
+ # Show next steps
1690
+ click.echo(f"\nNext steps:")
1691
+ click.echo(f" - View in S3: aws s3 ls {s3_base}/ --recursive")
1692
+ click.echo(f" - Download: aws s3 sync {s3_base}/ ./{config.agent_schema_ref.name}/{config.task}/")
1693
+
1694
+ except Exception as e:
1695
+ logger.error(f"Failed to export experiment: {e}")
1696
+ click.echo(f"Error: {e}", err=True)
1697
+ raise click.Abort()
@@ -12,12 +12,12 @@ from rem.services.content import ContentService
12
12
 
13
13
  @click.command(name="ingest")
14
14
  @click.argument("file_path", type=click.Path(exists=True))
15
- @click.option("--user-id", required=True, help="User ID to own the file")
15
+ @click.option("--user-id", default=None, help="User ID to scope file privately (default: public/shared)")
16
16
  @click.option("--category", help="Optional file category")
17
17
  @click.option("--tags", help="Optional comma-separated tags")
18
18
  def process_ingest(
19
19
  file_path: str,
20
- user_id: str,
20
+ user_id: str | None,
21
21
  category: str | None,
22
22
  tags: str | None,
23
23
  ):
@@ -32,8 +32,9 @@ def process_ingest(
32
32
  5. Creates a File entity record.
33
33
 
34
34
  Examples:
35
- rem process ingest sample.pdf --user-id user-123
36
- rem process ingest contract.docx --user-id user-123 --category legal --tags contract,2023
35
+ rem process ingest sample.pdf
36
+ rem process ingest contract.docx --category legal --tags contract,2023
37
+ rem process ingest agent.yaml # Auto-detects kind=agent, saves to schemas table
37
38
  """
38
39
  import asyncio
39
40
  from ...services.content import ContentService
@@ -56,7 +57,8 @@ def process_ingest(
56
57
 
57
58
  tag_list = tags.split(",") if tags else None
58
59
 
59
- logger.info(f"Ingesting file: {file_path} for user: {user_id}")
60
+ scope_msg = f"user: {user_id}" if user_id else "public"
61
+ logger.info(f"Ingesting file: {file_path} ({scope_msg})")
60
62
  result = await service.ingest_file(
61
63
  file_uri=file_path,
62
64
  user_id=user_id,
@@ -65,11 +67,15 @@ def process_ingest(
65
67
  is_local_server=True, # CLI is local
66
68
  )
67
69
 
68
- if result.get("processing_status") == "completed":
69
- logger.success(f"File ingested successfully: {result['file_name']}")
70
+ # Handle schema ingestion (agents/evaluators)
71
+ if result.get("schema_name"):
72
+ logger.success(f"Schema ingested: {result['schema_name']} (kind={result.get('kind', 'agent')})")
73
+ logger.info(f"Version: {result.get('version', '1.0.0')}")
74
+ # Handle file ingestion
75
+ elif result.get("processing_status") == "completed":
76
+ logger.success(f"File ingested: {result['file_name']}")
70
77
  logger.info(f"File ID: {result['file_id']}")
71
78
  logger.info(f"Resources created: {result['resources_created']}")
72
- logger.info(f"Status: {result['processing_status']}")
73
79
  else:
74
80
  logger.error(f"Ingestion failed: {result.get('message', 'Unknown error')}")
75
81
  sys.exit(1)