flock-core 0.3.23__py3-none-any.whl → 0.3.31__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 flock-core might be problematic. Click here for more details.

Files changed (38) hide show
  1. flock/__init__.py +23 -11
  2. flock/cli/constants.py +2 -4
  3. flock/cli/create_flock.py +220 -1
  4. flock/cli/execute_flock.py +200 -0
  5. flock/cli/load_flock.py +27 -7
  6. flock/cli/loaded_flock_cli.py +202 -0
  7. flock/cli/manage_agents.py +443 -0
  8. flock/cli/view_results.py +29 -0
  9. flock/cli/yaml_editor.py +283 -0
  10. flock/core/__init__.py +2 -2
  11. flock/core/api/__init__.py +11 -0
  12. flock/core/api/endpoints.py +222 -0
  13. flock/core/api/main.py +237 -0
  14. flock/core/api/models.py +34 -0
  15. flock/core/api/run_store.py +72 -0
  16. flock/core/api/ui/__init__.py +0 -0
  17. flock/core/api/ui/routes.py +271 -0
  18. flock/core/api/ui/utils.py +119 -0
  19. flock/core/flock.py +509 -388
  20. flock/core/flock_agent.py +384 -121
  21. flock/core/flock_registry.py +532 -0
  22. flock/core/logging/logging.py +97 -23
  23. flock/core/mixin/dspy_integration.py +363 -158
  24. flock/core/serialization/__init__.py +7 -1
  25. flock/core/serialization/callable_registry.py +52 -0
  26. flock/core/serialization/serializable.py +259 -37
  27. flock/core/serialization/serialization_utils.py +199 -0
  28. flock/evaluators/declarative/declarative_evaluator.py +2 -0
  29. flock/modules/memory/memory_module.py +17 -4
  30. flock/modules/output/output_module.py +9 -3
  31. flock/workflow/activities.py +2 -2
  32. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/METADATA +6 -3
  33. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/RECORD +36 -22
  34. flock/core/flock_api.py +0 -214
  35. flock/core/registry/agent_registry.py +0 -120
  36. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/WHEEL +0 -0
  37. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/entry_points.txt +0 -0
  38. {flock_core-0.3.23.dist-info → flock_core-0.3.31.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,283 @@
1
+ """YAML Editor for Flock CLI.
2
+
3
+ This module provides functionality to view, edit, and validate YAML configurations
4
+ for Flock and FlockAgent instances.
5
+ """
6
+
7
+ import os
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+
12
+ import questionary
13
+ import yaml
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.syntax import Syntax
17
+ from rich.table import Table
18
+
19
+ from flock.core.flock import Flock
20
+ from flock.core.flock_agent import FlockAgent
21
+ from flock.core.util.cli_helper import init_console
22
+
23
+ # Create console instance
24
+ console = Console()
25
+
26
+
27
+ def yaml_editor(flock_or_agent: Flock | FlockAgent | None = None):
28
+ """YAML Editor main entry point.
29
+
30
+ Args:
31
+ flock_or_agent: Optional Flock or FlockAgent instance to edit
32
+ """
33
+ init_console()
34
+ console.print(Panel("[bold green]YAML Editor[/]"), justify="center")
35
+
36
+ if flock_or_agent is None:
37
+ # If no object provided, provide options to load from file
38
+ _yaml_file_browser()
39
+ return
40
+
41
+ while True:
42
+ init_console()
43
+ console.print(Panel("[bold green]YAML Editor[/]"), justify="center")
44
+
45
+ # Determine object type
46
+ if isinstance(flock_or_agent, Flock):
47
+ obj_type = "Flock"
48
+ console.print(
49
+ f"Editing [bold cyan]Flock[/] with {len(flock_or_agent._agents)} agents"
50
+ )
51
+ elif isinstance(flock_or_agent, FlockAgent):
52
+ obj_type = "FlockAgent"
53
+ console.print(
54
+ f"Editing [bold cyan]FlockAgent[/]: {flock_or_agent.name}"
55
+ )
56
+ else:
57
+ console.print("[bold red]Error: Unknown object type[/]")
58
+ input("\nPress Enter to continue...")
59
+ return
60
+
61
+ console.line()
62
+
63
+ choice = questionary.select(
64
+ "What would you like to do?",
65
+ choices=[
66
+ questionary.Separator(line=" "),
67
+ "View Current YAML",
68
+ "Edit YAML Directly",
69
+ "Abstract Editor (Visual)",
70
+ "Validate YAML",
71
+ "Save to File",
72
+ questionary.Separator(),
73
+ "Back to Main Menu",
74
+ ],
75
+ ).ask()
76
+
77
+ if choice == "View Current YAML":
78
+ _view_yaml(flock_or_agent)
79
+ elif choice == "Edit YAML Directly":
80
+ flock_or_agent = _edit_yaml_directly(flock_or_agent)
81
+ elif choice == "Abstract Editor (Visual)":
82
+ flock_or_agent = _abstract_editor(flock_or_agent)
83
+ elif choice == "Validate YAML":
84
+ _validate_yaml(flock_or_agent)
85
+ elif choice == "Save to File":
86
+ _save_to_file(flock_or_agent)
87
+ elif choice == "Back to Main Menu":
88
+ break
89
+
90
+ if choice != "Back to Main Menu":
91
+ input("\nPress Enter to continue...")
92
+
93
+
94
+ def _yaml_file_browser():
95
+ """Browser for YAML files to load."""
96
+ console.print("\n[bold]YAML File Browser[/]")
97
+ console.line()
98
+
99
+ current_dir = os.getcwd()
100
+ console.print(f"Current directory: [cyan]{current_dir}[/]")
101
+
102
+ # List .yaml/.yml files in current directory
103
+ yaml_files = list(Path(current_dir).glob("*.yaml")) + list(
104
+ Path(current_dir).glob("*.yml")
105
+ )
106
+
107
+ if not yaml_files:
108
+ console.print("[yellow]No YAML files found in current directory.[/]")
109
+ input("\nPress Enter to continue...")
110
+ return
111
+
112
+ # Display files
113
+ table = Table(title="YAML Files")
114
+ table.add_column("Filename", style="cyan")
115
+ table.add_column("Size", style="green")
116
+ table.add_column("Last Modified", style="yellow")
117
+
118
+ for file in yaml_files:
119
+ table.add_row(
120
+ file.name, f"{file.stat().st_size} bytes", f"{file.stat().st_mtime}"
121
+ )
122
+
123
+ console.print(table)
124
+
125
+ # TODO: Add file selection and loading
126
+
127
+
128
+ def _view_yaml(obj: Flock | FlockAgent):
129
+ """View the YAML representation of an object.
130
+
131
+ Args:
132
+ obj: The object to view as YAML
133
+ """
134
+ yaml_str = obj.to_yaml()
135
+
136
+ # Display with syntax highlighting
137
+ syntax = Syntax(
138
+ yaml_str,
139
+ "yaml",
140
+ theme="monokai",
141
+ line_numbers=True,
142
+ code_width=100,
143
+ word_wrap=True,
144
+ )
145
+
146
+ init_console()
147
+ console.print(Panel("[bold green]YAML View[/]"), justify="center")
148
+ console.print(syntax)
149
+
150
+
151
+ def _edit_yaml_directly(obj: Flock | FlockAgent) -> Flock | FlockAgent:
152
+ """Edit the YAML representation directly using an external editor.
153
+
154
+ Args:
155
+ obj: The object to edit
156
+
157
+ Returns:
158
+ The updated object
159
+ """
160
+ # Convert to YAML
161
+ yaml_str = obj.to_yaml()
162
+
163
+ # Create a temporary file
164
+ with tempfile.NamedTemporaryFile(
165
+ suffix=".yaml", mode="w+", delete=False
166
+ ) as tmp:
167
+ tmp.write(yaml_str)
168
+ tmp_path = tmp.name
169
+
170
+ try:
171
+ # Determine which editor to use
172
+ editor = os.environ.get(
173
+ "EDITOR", "notepad" if os.name == "nt" else "nano"
174
+ )
175
+
176
+ # Open the editor
177
+ console.print(
178
+ f"\nOpening {editor} to edit YAML. Save and exit when done."
179
+ )
180
+ subprocess.call([editor, tmp_path])
181
+
182
+ # Read updated YAML
183
+ with open(tmp_path) as f:
184
+ updated_yaml = f.read()
185
+
186
+ # Parse back to object
187
+ try:
188
+ if isinstance(obj, Flock):
189
+ updated_obj = Flock.from_yaml(updated_yaml)
190
+ console.print("\n[green]✓[/] YAML parsed successfully!")
191
+ return updated_obj
192
+ elif isinstance(obj, FlockAgent):
193
+ updated_obj = FlockAgent.from_yaml(updated_yaml)
194
+ console.print("\n[green]✓[/] YAML parsed successfully!")
195
+ return updated_obj
196
+ except Exception as e:
197
+ console.print(f"\n[bold red]Error parsing YAML:[/] {e!s}")
198
+ console.print("\nKeeping original object.")
199
+ return obj
200
+
201
+ finally:
202
+ # Clean up the temporary file
203
+ try:
204
+ os.unlink(tmp_path)
205
+ except:
206
+ pass
207
+
208
+
209
+ def _abstract_editor(obj: Flock | FlockAgent) -> Flock | FlockAgent:
210
+ """Edit object using an abstract form-based editor.
211
+
212
+ Args:
213
+ obj: The object to edit
214
+
215
+ Returns:
216
+ The updated object
217
+ """
218
+ console.print("\n[yellow]Abstract visual editor not yet implemented.[/]")
219
+ console.print("Will provide a form-based editor for each field.")
220
+
221
+ # For now, just return the original object
222
+ return obj
223
+
224
+
225
+ def _validate_yaml(obj: Flock | FlockAgent):
226
+ """Validate the YAML representation of an object.
227
+
228
+ Args:
229
+ obj: The object to validate
230
+ """
231
+ try:
232
+ yaml_str = obj.to_yaml()
233
+
234
+ # Attempt to parse with PyYAML
235
+ yaml.safe_load(yaml_str)
236
+
237
+ # Attempt to deserialize back to object
238
+ if isinstance(obj, Flock):
239
+ Flock.from_yaml(yaml_str)
240
+ elif isinstance(obj, FlockAgent):
241
+ FlockAgent.from_yaml(yaml_str)
242
+
243
+ console.print("\n[green]✓[/] YAML validation successful!")
244
+ except Exception as e:
245
+ console.print(f"\n[bold red]YAML validation failed:[/] {e!s}")
246
+
247
+
248
+ def _save_to_file(obj: Flock | FlockAgent):
249
+ """Save object to a YAML file.
250
+
251
+ Args:
252
+ obj: The object to save
253
+ """
254
+ # Determine default filename based on object type
255
+ if isinstance(obj, Flock):
256
+ default_name = "my_flock.flock.yaml"
257
+ elif isinstance(obj, FlockAgent):
258
+ default_name = f"{obj.name}.agent.yaml"
259
+ else:
260
+ default_name = "unknown.yaml"
261
+
262
+ # Get file path
263
+ file_path = questionary.text(
264
+ "Enter file path to save YAML:",
265
+ default=default_name,
266
+ ).ask()
267
+
268
+ # Ensure the file has the correct extension
269
+ if not file_path.endswith((".yaml", ".yml")):
270
+ file_path += ".yaml"
271
+
272
+ # Create directory if it doesn't exist
273
+ save_path = Path(file_path)
274
+ save_path.parent.mkdir(parents=True, exist_ok=True)
275
+
276
+ try:
277
+ # Save to file
278
+ with open(file_path, "w") as f:
279
+ f.write(obj.to_yaml())
280
+
281
+ console.print(f"\n[green]✓[/] Saved to {file_path}")
282
+ except Exception as e:
283
+ console.print(f"\n[bold red]Error saving file:[/] {e!s}")
flock/core/__init__.py CHANGED
@@ -2,18 +2,18 @@
2
2
 
3
3
  from flock.core.flock import Flock
4
4
  from flock.core.flock_agent import FlockAgent
5
- from flock.core.flock_api import FlockAPI
6
5
  from flock.core.flock_evaluator import FlockEvaluator, FlockEvaluatorConfig
7
6
  from flock.core.flock_factory import FlockFactory
8
7
  from flock.core.flock_module import FlockModule, FlockModuleConfig
8
+ from flock.core.flock_registry import FlockRegistry
9
9
 
10
10
  __all__ = [
11
11
  "Flock",
12
- "FlockAPI",
13
12
  "FlockAgent",
14
13
  "FlockEvaluator",
15
14
  "FlockEvaluatorConfig",
16
15
  "FlockFactory",
17
16
  "FlockModule",
18
17
  "FlockModuleConfig",
18
+ "FlockRegistry",
19
19
  ]
@@ -0,0 +1,11 @@
1
+ # src/flock/core/api/__init__.py
2
+ """Flock API Server components."""
3
+
4
+ from .main import FlockAPI
5
+ from .models import FlockAPIRequest, FlockAPIResponse
6
+
7
+ __all__ = [
8
+ "FlockAPI",
9
+ "FlockAPIRequest",
10
+ "FlockAPIResponse",
11
+ ]
@@ -0,0 +1,222 @@
1
+ # src/flock/core/api/endpoints.py
2
+ """FastAPI endpoints for the Flock API."""
3
+
4
+ import html # For escaping
5
+ import uuid
6
+ from typing import TYPE_CHECKING # Added Any for type hinting clarity
7
+
8
+ from fastapi import (
9
+ APIRouter,
10
+ BackgroundTasks,
11
+ HTTPException,
12
+ Request as FastAPIRequest,
13
+ )
14
+
15
+ # Import HTMLResponse for the UI form endpoint
16
+ from fastapi.responses import HTMLResponse
17
+
18
+ from flock.core.logging.logging import get_logger
19
+
20
+ # Import models and UI utils
21
+ from .models import FlockAPIRequest, FlockAPIResponse
22
+
23
+ # Import UI utils - assuming they are now in ui/utils.py
24
+
25
+ # Use TYPE_CHECKING to avoid circular imports for type hints
26
+ if TYPE_CHECKING:
27
+ from flock.core.flock import Flock
28
+
29
+ from .main import FlockAPI
30
+ from .run_store import RunStore
31
+
32
+ logger = get_logger("api.endpoints")
33
+
34
+
35
+ # Factory function to create the router with dependencies
36
+ def create_api_router(flock_api: "FlockAPI") -> APIRouter:
37
+ """Creates the APIRouter and defines endpoints, injecting dependencies."""
38
+ router = APIRouter()
39
+ # Get dependencies from the main FlockAPI instance passed in
40
+ run_store: RunStore = flock_api.run_store
41
+ flock_instance: Flock = flock_api.flock
42
+
43
+ # --- API Endpoints ---
44
+ @router.post("/run/flock", response_model=FlockAPIResponse, tags=["API"])
45
+ async def run_flock_json(
46
+ request: FlockAPIRequest, background_tasks: BackgroundTasks
47
+ ):
48
+ """Run a flock workflow starting with the specified agent (expects JSON)."""
49
+ run_id = None
50
+ try:
51
+ run_id = str(uuid.uuid4())
52
+ run_store.create_run(run_id) # Use RunStore
53
+ response = run_store.get_run(
54
+ run_id
55
+ ) # Get initial response from store
56
+
57
+ processed_inputs = request.inputs if request.inputs else {}
58
+ logger.info(
59
+ f"API request: run flock '{request.agent_name}' (run_id: {run_id})",
60
+ inputs=processed_inputs,
61
+ )
62
+
63
+ if request.async_run:
64
+ logger.debug(
65
+ f"Running flock '{request.agent_name}' asynchronously (run_id: {run_id})"
66
+ )
67
+ # Call the helper method on the passed FlockAPI instance
68
+ background_tasks.add_task(
69
+ flock_api._run_flock,
70
+ run_id,
71
+ request.agent_name,
72
+ processed_inputs,
73
+ )
74
+ run_store.update_run_status(run_id, "running")
75
+ response.status = "running" # Update local response copy too
76
+ else:
77
+ logger.debug(
78
+ f"Running flock '{request.agent_name}' synchronously (run_id: {run_id})"
79
+ )
80
+ # Call the helper method on the passed FlockAPI instance
81
+ await flock_api._run_flock(
82
+ run_id, request.agent_name, processed_inputs
83
+ )
84
+ response = run_store.get_run(
85
+ run_id
86
+ ) # Fetch updated status/result
87
+
88
+ return response
89
+ except ValueError as ve:
90
+ logger.error(f"Value error starting run: {ve}")
91
+ if run_id:
92
+ run_store.update_run_status(run_id, "failed", str(ve))
93
+ raise HTTPException(status_code=400, detail=str(ve))
94
+ except Exception as e:
95
+ error_msg = f"Internal server error: {type(e).__name__}"
96
+ logger.error(f"Error starting run: {e!s}", exc_info=True)
97
+ if run_id:
98
+ run_store.update_run_status(run_id, "failed", error_msg)
99
+ raise HTTPException(status_code=500, detail=error_msg)
100
+
101
+ @router.get("/run/{run_id}", response_model=FlockAPIResponse, tags=["API"])
102
+ async def get_run_status(run_id: str):
103
+ """Get the status of a specific run."""
104
+ logger.debug(f"API request: get status for run_id: {run_id}")
105
+ run_data = run_store.get_run(run_id)
106
+ if not run_data:
107
+ logger.warning(f"Run ID not found: {run_id}")
108
+ raise HTTPException(status_code=404, detail="Run not found")
109
+ return run_data
110
+
111
+ @router.get("/agents", tags=["API"])
112
+ async def list_agents():
113
+ """List all available agents."""
114
+ logger.debug("API request: list agents")
115
+ # Access flock instance via factory closure
116
+ agents_list = [
117
+ {"name": agent.name, "description": agent.description or agent.name}
118
+ for agent in flock_instance.agents.values()
119
+ ]
120
+ return {"agents": agents_list}
121
+
122
+ # --- UI Form Endpoint ---
123
+ @router.post("/ui/run-agent-form", response_class=HTMLResponse, tags=["UI"])
124
+ async def run_flock_form(fastapi_req: FastAPIRequest):
125
+ """Endpoint to handle form submissions from the UI."""
126
+ run_id = None
127
+ try:
128
+ form_data = await fastapi_req.form()
129
+ agent_name = form_data.get("agent_name")
130
+ if not agent_name:
131
+ logger.warning("UI form submission missing agent_name")
132
+ return HTMLResponse(
133
+ '<div id="result-content" class="error-message">Error: Agent name not provided.</div>',
134
+ status_code=400,
135
+ )
136
+
137
+ logger.info(f"UI Form submission for agent: {agent_name}")
138
+ form_inputs = {}
139
+ # Access flock instance via factory closure
140
+ agent_def = flock_instance.agents.get(agent_name)
141
+ # Use helper from flock_api instance for parsing
142
+ defined_input_fields = (
143
+ flock_api._parse_input_spec(agent_def.input or "")
144
+ if agent_def
145
+ else []
146
+ )
147
+
148
+ for key, value in form_data.items():
149
+ if key.startswith("inputs."):
150
+ form_inputs[key[len("inputs.") :]] = value
151
+ for field in defined_input_fields: # Handle checkboxes
152
+ if (
153
+ field["html_type"] == "checkbox"
154
+ and field["name"] not in form_inputs
155
+ ):
156
+ form_inputs[field["name"]] = False
157
+ elif (
158
+ field["html_type"] == "checkbox"
159
+ and field["name"] in form_inputs
160
+ ):
161
+ form_inputs[field["name"]] = True
162
+
163
+ logger.debug(f"Parsed form inputs for UI run: {form_inputs}")
164
+ run_id = str(uuid.uuid4())
165
+ run_store.create_run(run_id) # Use RunStore
166
+ logger.debug(
167
+ f"Running flock '{agent_name}' synchronously from UI (run_id: {run_id})"
168
+ )
169
+
170
+ # Call helper method on flock_api instance
171
+ await flock_api._run_flock(run_id, agent_name, form_inputs)
172
+
173
+ final_status = run_store.get_run(run_id)
174
+ if final_status and final_status.status == "completed":
175
+ # Use helper from flock_api instance for formatting
176
+ formatted_html = flock_api._format_result_to_html(
177
+ final_status.result
178
+ )
179
+ logger.info(f"UI run completed successfully (run_id: {run_id})")
180
+ return HTMLResponse(
181
+ f"<div id='result-content'>{formatted_html}</div>"
182
+ ) # Wrap in target div
183
+ elif final_status and final_status.status == "failed":
184
+ logger.error(
185
+ f"UI run failed (run_id: {run_id}): {final_status.error}"
186
+ )
187
+ error_msg = html.escape(final_status.error or "Unknown error")
188
+ return HTMLResponse(
189
+ f"<div id='result-content' class='error-message'>Run Failed: {error_msg}</div>",
190
+ status_code=500,
191
+ )
192
+ else:
193
+ status_str = (
194
+ final_status.status if final_status else "Not Found"
195
+ )
196
+ logger.warning(
197
+ f"UI run {run_id} ended in unexpected state: {status_str}"
198
+ )
199
+ return HTMLResponse(
200
+ f"<div id='result-content' class='error-message'>Run ended unexpectedly. Status: {status_str}</div>",
201
+ status_code=500,
202
+ )
203
+
204
+ except ValueError as ve:
205
+ logger.error(f"Value error processing UI form run: {ve}")
206
+ if run_id:
207
+ run_store.update_run_status(run_id, "failed", str(ve))
208
+ return HTMLResponse(
209
+ f"<div id='result-content' class='error-message'>Error: {html.escape(str(ve))}</div>",
210
+ status_code=400,
211
+ )
212
+ except Exception as e:
213
+ error_msg = f"Internal server error: {type(e).__name__}"
214
+ logger.error(f"Error processing UI form run: {e!s}", exc_info=True)
215
+ if run_id:
216
+ run_store.update_run_status(run_id, "failed", error_msg)
217
+ return HTMLResponse(
218
+ f"<div id='result-content' class='error-message'>{html.escape(error_msg)}</div>",
219
+ status_code=500,
220
+ )
221
+
222
+ return router