flock-core 0.1.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 flock-core might be problematic. Click here for more details.
- flock/__init__.py +4 -0
- flock/agents/__init__.py +3 -0
- flock/agents/batch_agent.py +175 -0
- flock/agents/declarative_agent.py +166 -0
- flock/agents/loop_agent.py +178 -0
- flock/agents/trigger_agent.py +191 -0
- flock/agents/user_agent.py +230 -0
- flock/app/components/__init__.py +14 -0
- flock/app/components/charts/agent_workflow.py +14 -0
- flock/app/components/charts/core_architecture.py +14 -0
- flock/app/components/charts/tool_system.py +14 -0
- flock/app/components/history_grid.py +168 -0
- flock/app/components/history_grid_alt.py +189 -0
- flock/app/components/sidebar.py +19 -0
- flock/app/components/theme.py +9 -0
- flock/app/components/util.py +18 -0
- flock/app/hive_app.py +118 -0
- flock/app/html/d3.html +179 -0
- flock/app/modules/__init__.py +12 -0
- flock/app/modules/about.py +17 -0
- flock/app/modules/agent_detail.py +70 -0
- flock/app/modules/agent_list.py +59 -0
- flock/app/modules/playground.py +322 -0
- flock/app/modules/settings.py +96 -0
- flock/core/__init__.py +7 -0
- flock/core/agent.py +150 -0
- flock/core/agent_registry.py +162 -0
- flock/core/config/declarative_agent_config.py +0 -0
- flock/core/context.py +279 -0
- flock/core/context_vars.py +6 -0
- flock/core/flock.py +208 -0
- flock/core/handoff/handoff_base.py +12 -0
- flock/core/logging/__init__.py +18 -0
- flock/core/logging/error_handler.py +84 -0
- flock/core/logging/formatters.py +122 -0
- flock/core/logging/handlers.py +117 -0
- flock/core/logging/logger.py +107 -0
- flock/core/serializable.py +206 -0
- flock/core/tools/basic_tools.py +98 -0
- flock/workflow/activities.py +115 -0
- flock/workflow/agent_activities.py +26 -0
- flock/workflow/temporal_setup.py +37 -0
- flock/workflow/workflow.py +53 -0
- flock_core-0.1.1.dist-info/METADATA +449 -0
- flock_core-0.1.1.dist-info/RECORD +48 -0
- flock_core-0.1.1.dist-info/WHEEL +4 -0
- flock_core-0.1.1.dist-info/entry_points.txt +2 -0
- flock_core-0.1.1.dist-info/licenses/LICENSE +21 -0
flock/core/flock.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# src/your_package/core/manager.py
|
|
2
|
+
import contextlib
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
from flock.core.agent_registry import Registry
|
|
8
|
+
from flock.core.context import FlockContext
|
|
9
|
+
from flock.core.context_vars import FLOCK_CURRENT_AGENT, FLOCK_INITIAL_INPUT, FLOCK_LOCAL_DEBUG, FLOCK_RUN_ID
|
|
10
|
+
from flock.core.logging import flock_logger, performance_handler
|
|
11
|
+
from flock.core.logging.formatters import AgentResultFormatter
|
|
12
|
+
from flock.core.logging.logger import FlockLogger, LogLevel
|
|
13
|
+
from flock.workflow.activities import run_agent
|
|
14
|
+
from flock.workflow.temporal_setup import create_temporal_client, setup_worker
|
|
15
|
+
from flock.workflow.workflow import FlockWorkflow
|
|
16
|
+
|
|
17
|
+
from .agent import Agent
|
|
18
|
+
|
|
19
|
+
T = TypeVar("T", bound=Agent)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Flock:
|
|
23
|
+
"""Manages creation and execution of agents.
|
|
24
|
+
This is a high-level class that the user interacts with directly.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
model: str = "openai/gpt-4o",
|
|
30
|
+
log_level: int = LogLevel.MINIMAL,
|
|
31
|
+
enable_performance_tracking: bool = False,
|
|
32
|
+
local_debug: bool = False,
|
|
33
|
+
):
|
|
34
|
+
"""Initialize the Flock.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
model: The default model to use for agents.
|
|
38
|
+
log_level: The logging verbosity level:
|
|
39
|
+
- LogLevel.NONE: No logging except errors
|
|
40
|
+
- LogLevel.MINIMAL: Only agent outputs (default)
|
|
41
|
+
- LogLevel.BASIC: Agent outputs + basic workflow info
|
|
42
|
+
- LogLevel.VERBOSE: Everything including debug info
|
|
43
|
+
log_format: The logging format to use:
|
|
44
|
+
- LogFormat.SIMPLE: Simple message format (default)
|
|
45
|
+
- LogFormat.STRUCTURED: Table-like structured format
|
|
46
|
+
enable_performance_tracking: Whether to track and display performance metrics.
|
|
47
|
+
local_debug: Whether to run in local debug mode (default: False).
|
|
48
|
+
"""
|
|
49
|
+
self.agents: dict[str, Agent] = {}
|
|
50
|
+
self.registry = Registry()
|
|
51
|
+
self.context = FlockContext()
|
|
52
|
+
self.model = model
|
|
53
|
+
self.enable_performance_tracking = enable_performance_tracking
|
|
54
|
+
self.local_debug = local_debug
|
|
55
|
+
|
|
56
|
+
# Configure performance tracking
|
|
57
|
+
if enable_performance_tracking:
|
|
58
|
+
performance_handler.enable()
|
|
59
|
+
else:
|
|
60
|
+
performance_handler.disable()
|
|
61
|
+
|
|
62
|
+
# Set LOCAL_DEBUG environment variable for console detection
|
|
63
|
+
if local_debug:
|
|
64
|
+
os.environ["LOCAL_DEBUG"] = "1"
|
|
65
|
+
elif "LOCAL_DEBUG" in os.environ:
|
|
66
|
+
del os.environ["LOCAL_DEBUG"]
|
|
67
|
+
|
|
68
|
+
# Configure logging
|
|
69
|
+
global flock_logger
|
|
70
|
+
flock_logger = FlockLogger(level=log_level)
|
|
71
|
+
flock_logger.info("Initialized Flock", model=model, local_debug=local_debug)
|
|
72
|
+
|
|
73
|
+
def add_agent(self, agent: T) -> T:
|
|
74
|
+
if not agent.model:
|
|
75
|
+
agent.model = self.model
|
|
76
|
+
|
|
77
|
+
if agent.name in self.agents:
|
|
78
|
+
flock_logger.debug(f"Agent {agent.name} already exists, returning existing instance")
|
|
79
|
+
return self.agents[agent.name]
|
|
80
|
+
|
|
81
|
+
self.agents[agent.name] = agent
|
|
82
|
+
self.registry.register_agent(agent)
|
|
83
|
+
self.context.add_agent_definition(type(agent), agent.name, agent.to_dict())
|
|
84
|
+
if hasattr(agent, "tools") and agent.tools:
|
|
85
|
+
for tool in agent.tools:
|
|
86
|
+
self.registry.register_tool(tool.__name__, tool)
|
|
87
|
+
flock_logger.debug(f"Registered tool: {tool.__name__}")
|
|
88
|
+
|
|
89
|
+
flock_logger.info(f"Added agent: {agent.name}", model=agent.model)
|
|
90
|
+
return agent
|
|
91
|
+
|
|
92
|
+
def add_tool(self, tool_name: str, tool: callable):
|
|
93
|
+
self.registry.register_tool(tool_name, tool)
|
|
94
|
+
flock_logger.debug(f"Registered tool: {tool_name}")
|
|
95
|
+
|
|
96
|
+
async def _run_local_debug_workflow(self, box_result: bool = True):
|
|
97
|
+
if self.enable_performance_tracking:
|
|
98
|
+
perf_context = performance_handler.track_time("local_workflow")
|
|
99
|
+
else:
|
|
100
|
+
perf_context = contextlib.nullcontext()
|
|
101
|
+
|
|
102
|
+
with perf_context:
|
|
103
|
+
flock_logger.info("Running workflow in local debug mode")
|
|
104
|
+
result = await run_agent(self.context)
|
|
105
|
+
|
|
106
|
+
# Format and display the result (always shown regardless of log level)
|
|
107
|
+
agent_name = self.context.get_variable(FLOCK_CURRENT_AGENT)
|
|
108
|
+
AgentResultFormatter.print_result(result, agent_name)
|
|
109
|
+
|
|
110
|
+
if self.enable_performance_tracking:
|
|
111
|
+
performance_handler.display_timings()
|
|
112
|
+
|
|
113
|
+
if box_result:
|
|
114
|
+
from box import Box
|
|
115
|
+
|
|
116
|
+
flock_logger.debug("Boxing result")
|
|
117
|
+
return Box(result)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
async def _run_temporal_workflow(self, box_result: bool = True):
|
|
121
|
+
if self.enable_performance_tracking:
|
|
122
|
+
perf_context = performance_handler.track_time("temporal_workflow")
|
|
123
|
+
else:
|
|
124
|
+
perf_context = contextlib.nullcontext()
|
|
125
|
+
|
|
126
|
+
with perf_context:
|
|
127
|
+
flock_logger.info("Connecting to Temporal and starting worker...")
|
|
128
|
+
|
|
129
|
+
await setup_worker(workflow=FlockWorkflow, activity=run_agent)
|
|
130
|
+
|
|
131
|
+
flock_logger.info("Starting workflow execution")
|
|
132
|
+
|
|
133
|
+
flock_client = await create_temporal_client()
|
|
134
|
+
result = await flock_client.execute_workflow(
|
|
135
|
+
FlockWorkflow.run,
|
|
136
|
+
self.context.to_dict(),
|
|
137
|
+
id=self.context.get_variable(FLOCK_RUN_ID),
|
|
138
|
+
task_queue="flock-queue",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Format and display the result (always shown regardless of log level)
|
|
142
|
+
agent_name = self.context.get_variable(FLOCK_CURRENT_AGENT)
|
|
143
|
+
flock_logger.result(result, agent_name)
|
|
144
|
+
|
|
145
|
+
if self.enable_performance_tracking:
|
|
146
|
+
performance_handler.display_timings()
|
|
147
|
+
|
|
148
|
+
if box_result:
|
|
149
|
+
from box import Box
|
|
150
|
+
|
|
151
|
+
flock_logger.debug("Boxing result")
|
|
152
|
+
return Box(result)
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
async def run_async(
|
|
156
|
+
self,
|
|
157
|
+
start_agent: Agent | str,
|
|
158
|
+
input: str,
|
|
159
|
+
context: FlockContext = None,
|
|
160
|
+
run_id: str = "",
|
|
161
|
+
box_result: bool = True,
|
|
162
|
+
) -> dict:
|
|
163
|
+
"""Entry point for running an agent system."""
|
|
164
|
+
try:
|
|
165
|
+
if self.enable_performance_tracking:
|
|
166
|
+
perf_context = performance_handler.track_time("run_setup")
|
|
167
|
+
else:
|
|
168
|
+
perf_context = contextlib.nullcontext()
|
|
169
|
+
|
|
170
|
+
with perf_context:
|
|
171
|
+
if isinstance(start_agent, str):
|
|
172
|
+
flock_logger.debug(f"Looking up agent by name: {start_agent}")
|
|
173
|
+
start_agent = self.registry.get_agent(start_agent)
|
|
174
|
+
if not start_agent:
|
|
175
|
+
raise ValueError(f"Agent '{start_agent}' not found in registry")
|
|
176
|
+
|
|
177
|
+
if context:
|
|
178
|
+
flock_logger.debug("Using provided context")
|
|
179
|
+
self.context = context
|
|
180
|
+
|
|
181
|
+
flock_logger.info("Setting up run context", agent=start_agent.name, input=input)
|
|
182
|
+
self.context.set_variable(FLOCK_CURRENT_AGENT, start_agent.name)
|
|
183
|
+
self.context.set_variable(FLOCK_INITIAL_INPUT, input)
|
|
184
|
+
self.context.set_variable(FLOCK_LOCAL_DEBUG, self.local_debug)
|
|
185
|
+
|
|
186
|
+
if not run_id:
|
|
187
|
+
run_id = start_agent.name + "_" + uuid.uuid4().hex[:4]
|
|
188
|
+
flock_logger.debug(f"Generated run ID: {run_id}")
|
|
189
|
+
|
|
190
|
+
self.context.set_variable(FLOCK_RUN_ID, run_id)
|
|
191
|
+
|
|
192
|
+
if self.enable_performance_tracking:
|
|
193
|
+
performance_handler.display_timings()
|
|
194
|
+
|
|
195
|
+
# we can run the flow locally for debugging purposes
|
|
196
|
+
if self.local_debug:
|
|
197
|
+
return await self._run_local_debug_workflow(box_result)
|
|
198
|
+
else:
|
|
199
|
+
return await self._run_temporal_workflow(box_result)
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
flock_logger.error(
|
|
203
|
+
"Run failed",
|
|
204
|
+
error=str(e),
|
|
205
|
+
agent=getattr(start_agent, "name", str(start_agent)),
|
|
206
|
+
input=input,
|
|
207
|
+
)
|
|
208
|
+
raise
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from flock.core.agent import Agent
|
|
4
|
+
from flock.core.context import FlockContext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HandoffBase:
|
|
8
|
+
"""Base class for handoff implementations."""
|
|
9
|
+
|
|
10
|
+
next_agent: str | Agent
|
|
11
|
+
input: dict[str, Any]
|
|
12
|
+
context: FlockContext
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Flock logging system with Rich integration and Temporal compatibility."""
|
|
2
|
+
|
|
3
|
+
from flock.core.logging.error_handler import error_handler
|
|
4
|
+
from flock.core.logging.formatters import PerformanceFormatter, StructuredFormatter
|
|
5
|
+
from flock.core.logging.handlers import live_update_handler, performance_handler
|
|
6
|
+
from flock.core.logging.logger import flock_logger
|
|
7
|
+
|
|
8
|
+
# Install the Rich error handler by default
|
|
9
|
+
error_handler.install()
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"PerformanceFormatter",
|
|
13
|
+
"StructuredFormatter",
|
|
14
|
+
"error_handler",
|
|
15
|
+
"flock_logger",
|
|
16
|
+
"live_update_handler",
|
|
17
|
+
"performance_handler",
|
|
18
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from types import TracebackType
|
|
3
|
+
|
|
4
|
+
from temporalio import workflow
|
|
5
|
+
|
|
6
|
+
with workflow.unsafe.imports_passed_through():
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
from rich.traceback import Traceback
|
|
11
|
+
|
|
12
|
+
from flock.core.logging.logger import flock_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ErrorHandler:
|
|
16
|
+
"""Handles error formatting and display using Rich."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, console: Console | None = None):
|
|
19
|
+
self.console = console or Console()
|
|
20
|
+
|
|
21
|
+
def format_exception(
|
|
22
|
+
self,
|
|
23
|
+
exc_type: type[BaseException],
|
|
24
|
+
exc_value: BaseException,
|
|
25
|
+
exc_tb: TracebackType,
|
|
26
|
+
*,
|
|
27
|
+
show_locals: bool = True,
|
|
28
|
+
) -> Panel:
|
|
29
|
+
"""Format an exception with Rich styling."""
|
|
30
|
+
# Create a Rich traceback
|
|
31
|
+
rich_tb = Traceback.from_exception(
|
|
32
|
+
exc_type,
|
|
33
|
+
exc_value,
|
|
34
|
+
exc_tb,
|
|
35
|
+
show_locals=show_locals,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# If there's source code available, syntax highlight it
|
|
39
|
+
if exc_tb and exc_tb.tb_frame.f_code.co_filename != "<string>":
|
|
40
|
+
try:
|
|
41
|
+
with open(exc_tb.tb_frame.f_code.co_filename) as f:
|
|
42
|
+
source = f.read()
|
|
43
|
+
syntax = Syntax(
|
|
44
|
+
source,
|
|
45
|
+
"python",
|
|
46
|
+
line_numbers=True,
|
|
47
|
+
highlight_lines={exc_tb.tb_lineno},
|
|
48
|
+
)
|
|
49
|
+
except:
|
|
50
|
+
syntax = None
|
|
51
|
+
else:
|
|
52
|
+
syntax = None
|
|
53
|
+
|
|
54
|
+
# Create a panel with the traceback
|
|
55
|
+
return Panel(
|
|
56
|
+
rich_tb,
|
|
57
|
+
title=f"[red]{exc_type.__name__}[/]: {exc_value!s}",
|
|
58
|
+
border_style="red",
|
|
59
|
+
padding=(1, 2),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def handle_exception(
|
|
63
|
+
self,
|
|
64
|
+
exc_type: type[BaseException],
|
|
65
|
+
exc_value: BaseException,
|
|
66
|
+
exc_tb: TracebackType,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Handle an exception by formatting and displaying it."""
|
|
69
|
+
panel = self.format_exception(exc_type, exc_value, exc_tb)
|
|
70
|
+
self.console.print(panel)
|
|
71
|
+
# Also log the error through our logger
|
|
72
|
+
flock_logger.error(
|
|
73
|
+
f"Exception occurred: {exc_type.__name__}: {exc_value!s}",
|
|
74
|
+
error_type=exc_type.__name__,
|
|
75
|
+
error_details=str(exc_value),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def install(self) -> None:
|
|
79
|
+
"""Install this error handler as the default exception handler."""
|
|
80
|
+
sys.excepthook = self.handle_exception
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Global error handler instance
|
|
84
|
+
error_handler = ErrorHandler()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from devtools import pprint
|
|
4
|
+
from temporalio import workflow
|
|
5
|
+
|
|
6
|
+
with workflow.unsafe.imports_passed_through():
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StructuredFormatter:
|
|
14
|
+
"""Formats structured data for Rich output."""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def create_status_table(data: dict[str, Any]) -> Table:
|
|
18
|
+
"""Create a Rich table for displaying status information."""
|
|
19
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
20
|
+
table.add_column("Key")
|
|
21
|
+
table.add_column("Value")
|
|
22
|
+
|
|
23
|
+
for key, value in data.items():
|
|
24
|
+
table.add_row(str(key), str(value))
|
|
25
|
+
|
|
26
|
+
return table
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def create_progress_bar(description: str = "Progress") -> Progress:
|
|
30
|
+
"""Create a Rich progress bar with time tracking."""
|
|
31
|
+
return Progress(
|
|
32
|
+
SpinnerColumn(),
|
|
33
|
+
TextColumn("[progress.description]{task.description}"),
|
|
34
|
+
BarColumn(),
|
|
35
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
36
|
+
TimeElapsedColumn(),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def create_workflow_panel(
|
|
41
|
+
workflow_id: str, status: str, details: dict[str, Any], title: str = "Workflow Status"
|
|
42
|
+
) -> Panel:
|
|
43
|
+
"""Create a Rich panel for workflow information."""
|
|
44
|
+
table = StructuredFormatter.create_status_table({"Workflow ID": workflow_id, "Status": status, **details})
|
|
45
|
+
return Panel(table, title=title, border_style="blue")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def create_activity_panel(
|
|
49
|
+
activity_id: str, name: str, status: str, details: dict[str, Any], title: str = "Activity Status"
|
|
50
|
+
) -> Panel:
|
|
51
|
+
"""Create a Rich panel for activity information."""
|
|
52
|
+
table = StructuredFormatter.create_status_table(
|
|
53
|
+
{"Activity ID": activity_id, "Name": name, "Status": status, **details}
|
|
54
|
+
)
|
|
55
|
+
return Panel(table, title=title, border_style="magenta")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PerformanceFormatter:
|
|
59
|
+
"""Formats performance metrics for Rich output."""
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def create_timing_tree(timings: dict[str, float]) -> str:
|
|
63
|
+
"""Create a tree-like structure showing execution timings."""
|
|
64
|
+
if not timings:
|
|
65
|
+
return "No timings recorded"
|
|
66
|
+
|
|
67
|
+
result = "Performance Metrics:\n"
|
|
68
|
+
for operation, duration in timings.items():
|
|
69
|
+
result += f"├── {operation}: {duration:.3f}s\n"
|
|
70
|
+
return result[:-1] # Remove trailing newline
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def create_performance_panel(metrics: dict[str, Any], title: str = "Performance Metrics") -> Panel:
|
|
74
|
+
"""Create a panel showing performance metrics."""
|
|
75
|
+
table = StructuredFormatter.create_status_table(metrics)
|
|
76
|
+
return Panel(table, title=title, border_style="cyan")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AgentResultFormatter:
|
|
80
|
+
"""Formats agent results in a beautiful Rich table."""
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def format_result(result: dict[str, Any], agent_name: str) -> Panel:
|
|
84
|
+
"""Format an agent's result as a Rich panel containing a table."""
|
|
85
|
+
# Create a table with a nice header
|
|
86
|
+
table = Table(
|
|
87
|
+
show_header=True,
|
|
88
|
+
header_style="bold green",
|
|
89
|
+
title=f"Agent Results: {agent_name}",
|
|
90
|
+
title_style="bold blue",
|
|
91
|
+
border_style="bright_blue",
|
|
92
|
+
)
|
|
93
|
+
table.add_column("Output", style="cyan")
|
|
94
|
+
table.add_column("Value", style="green")
|
|
95
|
+
|
|
96
|
+
# Add each result to the table
|
|
97
|
+
for key, value in result.items():
|
|
98
|
+
# Format multi-line values (like blog headers) nicely
|
|
99
|
+
if isinstance(value, (list, tuple)) or (isinstance(value, str) and "\n" in value):
|
|
100
|
+
formatted_value = "\n".join(value) if isinstance(value, (list, tuple)) else value
|
|
101
|
+
# Add some padding for multi-line values
|
|
102
|
+
table.add_row(key, f"\n{formatted_value}\n")
|
|
103
|
+
else:
|
|
104
|
+
table.add_row(key, str(value))
|
|
105
|
+
|
|
106
|
+
pprint(result)
|
|
107
|
+
|
|
108
|
+
# Wrap the table in a panel for a nice border
|
|
109
|
+
return Panel(
|
|
110
|
+
table,
|
|
111
|
+
title="🎯 Agent Output",
|
|
112
|
+
title_align="left",
|
|
113
|
+
border_style="blue",
|
|
114
|
+
padding=(1, 2),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def print_result(result: dict[str, Any], agent_name: str, console: Console | None = None) -> None:
|
|
119
|
+
"""Print an agent's result using Rich formatting."""
|
|
120
|
+
console = console or Console()
|
|
121
|
+
panel = AgentResultFormatter.format_result(result, agent_name)
|
|
122
|
+
console.print(panel)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from temporalio import workflow
|
|
4
|
+
|
|
5
|
+
with workflow.unsafe.imports_passed_through():
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.live import Live
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable, Generator
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from functools import wraps
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
|
|
14
|
+
from flock.core.logging.formatters import PerformanceFormatter, StructuredFormatter
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PerformanceHandler:
|
|
20
|
+
"""Handles performance tracking and reporting."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, console: Console | None = None):
|
|
23
|
+
self.console = console or Console()
|
|
24
|
+
self.timings: dict[str, float] = {}
|
|
25
|
+
self.enabled = False
|
|
26
|
+
|
|
27
|
+
def enable(self):
|
|
28
|
+
"""Enable performance tracking."""
|
|
29
|
+
self.enabled = True
|
|
30
|
+
|
|
31
|
+
def disable(self):
|
|
32
|
+
"""Disable performance tracking."""
|
|
33
|
+
self.enabled = False
|
|
34
|
+
|
|
35
|
+
def _get_time(self) -> float:
|
|
36
|
+
"""Get current time in a workflow-safe way."""
|
|
37
|
+
try:
|
|
38
|
+
# Try to get workflow time first
|
|
39
|
+
return workflow.now().timestamp()
|
|
40
|
+
except workflow._NotInWorkflowEventLoopError:
|
|
41
|
+
# Fall back to system time if not in workflow
|
|
42
|
+
return time.time()
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def track_time(self, operation_name: str) -> Generator[None, None, None]:
|
|
46
|
+
"""Context manager for tracking operation execution time."""
|
|
47
|
+
if not self.enabled:
|
|
48
|
+
yield
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
start_time = self._get_time()
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
end_time = self._get_time()
|
|
56
|
+
duration = end_time - start_time
|
|
57
|
+
self.timings[operation_name] = duration
|
|
58
|
+
|
|
59
|
+
def track_operation(self, operation_name: str) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
|
60
|
+
"""Decorator for tracking function execution time."""
|
|
61
|
+
|
|
62
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
|
63
|
+
@wraps(func)
|
|
64
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
65
|
+
with self.track_time(operation_name):
|
|
66
|
+
return func(*args, **kwargs)
|
|
67
|
+
|
|
68
|
+
return wrapper
|
|
69
|
+
|
|
70
|
+
return decorator
|
|
71
|
+
|
|
72
|
+
def display_timings(self) -> None:
|
|
73
|
+
"""Display all tracked timings in a tree format."""
|
|
74
|
+
if self.timings:
|
|
75
|
+
tree = PerformanceFormatter.create_timing_tree(self.timings)
|
|
76
|
+
self.console.print(tree)
|
|
77
|
+
|
|
78
|
+
def clear_timings(self) -> None:
|
|
79
|
+
"""Clear all tracked timings."""
|
|
80
|
+
self.timings.clear()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LiveUpdateHandler:
|
|
84
|
+
"""Handles live updates for long-running operations."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, console: Console | None = None):
|
|
87
|
+
self.console = console or Console()
|
|
88
|
+
|
|
89
|
+
@contextmanager
|
|
90
|
+
def progress_tracker(self, description: str = "Progress") -> Generator[Any, None, None]:
|
|
91
|
+
"""Context manager for tracking progress with a live display."""
|
|
92
|
+
progress = StructuredFormatter.create_progress_bar(description)
|
|
93
|
+
with Live(progress, console=self.console, refresh_per_second=4) as live:
|
|
94
|
+
task = progress.add_task(description, total=100)
|
|
95
|
+
try:
|
|
96
|
+
yield lambda p: progress.update(task, completed=p)
|
|
97
|
+
finally:
|
|
98
|
+
progress.update(task, completed=100)
|
|
99
|
+
|
|
100
|
+
def update_workflow_status(
|
|
101
|
+
self, workflow_id: str, status: str, details: dict[str, Any], refresh_per_second: int = 1
|
|
102
|
+
) -> Live:
|
|
103
|
+
"""Create a live updating workflow status panel."""
|
|
104
|
+
panel = StructuredFormatter.create_workflow_panel(workflow_id, status, details)
|
|
105
|
+
return Live(panel, console=self.console, refresh_per_second=refresh_per_second)
|
|
106
|
+
|
|
107
|
+
def update_activity_status(
|
|
108
|
+
self, activity_id: str, name: str, status: str, details: dict[str, Any], refresh_per_second: int = 1
|
|
109
|
+
) -> Live:
|
|
110
|
+
"""Create a live updating activity status panel."""
|
|
111
|
+
panel = StructuredFormatter.create_activity_panel(activity_id, name, status, details)
|
|
112
|
+
return Live(panel, console=self.console, refresh_per_second=refresh_per_second)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Global instances for convenience
|
|
116
|
+
performance_handler = PerformanceHandler()
|
|
117
|
+
live_update_handler = LiveUpdateHandler()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from temporalio import workflow
|
|
4
|
+
|
|
5
|
+
from flock.core.logging.formatters import AgentResultFormatter
|
|
6
|
+
|
|
7
|
+
with workflow.unsafe.imports_passed_through():
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.theme import Theme
|
|
10
|
+
|
|
11
|
+
# Custom theme for different log levels
|
|
12
|
+
THEME = Theme({
|
|
13
|
+
"info": "cyan",
|
|
14
|
+
"warning": "yellow",
|
|
15
|
+
"error": "red",
|
|
16
|
+
"debug": "grey50",
|
|
17
|
+
"success": "green",
|
|
18
|
+
"workflow": "blue",
|
|
19
|
+
"activity": "magenta",
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LogLevel:
|
|
24
|
+
NONE = 0 # No logging except errors
|
|
25
|
+
MINIMAL = 1 # Only agent outputs
|
|
26
|
+
BASIC = 2 # Agent outputs + basic workflow info
|
|
27
|
+
VERBOSE = 3 # Everything including debug info
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FlockLogger:
|
|
31
|
+
"""Custom logger for Flock that integrates with Rich and respects Temporal constraints."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, name: str = "flock", console: Console | None = None, level: int = LogLevel.MINIMAL):
|
|
34
|
+
self.name = name
|
|
35
|
+
self.console = console or Console(theme=THEME)
|
|
36
|
+
self._workflow_id: str | None = None
|
|
37
|
+
self._activity_id: str | None = None
|
|
38
|
+
self.level = level
|
|
39
|
+
|
|
40
|
+
def set_level(self, level: int) -> None:
|
|
41
|
+
"""Set the logging level."""
|
|
42
|
+
self.level = level
|
|
43
|
+
|
|
44
|
+
def set_context(self, workflow_id: str | None = None, activity_id: str | None = None) -> None:
|
|
45
|
+
"""Set the current workflow and activity context."""
|
|
46
|
+
self._workflow_id = workflow_id
|
|
47
|
+
self._activity_id = activity_id
|
|
48
|
+
|
|
49
|
+
def _format_message(self, level: str, message: str, **kwargs: Any) -> str:
|
|
50
|
+
"""Format log message with context but without timestamps (for Temporal compatibility)."""
|
|
51
|
+
context_parts = []
|
|
52
|
+
if self._workflow_id:
|
|
53
|
+
context_parts.append(f"[workflow]workflow={self._workflow_id}[/]")
|
|
54
|
+
if self._activity_id:
|
|
55
|
+
context_parts.append(f"[activity]activity={self._activity_id}[/]")
|
|
56
|
+
|
|
57
|
+
# Add any additional context from kwargs
|
|
58
|
+
for key, value in kwargs.items():
|
|
59
|
+
if key not in ("workflow_id", "activity_id"):
|
|
60
|
+
context_parts.append(f"{key}={value}")
|
|
61
|
+
|
|
62
|
+
context_str = " ".join(context_parts)
|
|
63
|
+
return f"[{level}]{level.upper()}[/] {message} {context_str if context_str else ''}"
|
|
64
|
+
|
|
65
|
+
def info(self, message: str, **kwargs: Any) -> None:
|
|
66
|
+
"""Log an info message."""
|
|
67
|
+
if self.level >= LogLevel.BASIC:
|
|
68
|
+
self.console.print(self._format_message("info", message, **kwargs))
|
|
69
|
+
|
|
70
|
+
def warning(self, message: str, **kwargs: Any) -> None:
|
|
71
|
+
"""Log a warning message."""
|
|
72
|
+
if self.level >= LogLevel.BASIC:
|
|
73
|
+
self.console.print(self._format_message("warning", message, **kwargs))
|
|
74
|
+
|
|
75
|
+
def error(self, message: str, **kwargs: Any) -> None:
|
|
76
|
+
"""Log an error message."""
|
|
77
|
+
# Errors are always logged regardless of level
|
|
78
|
+
self.console.print(self._format_message("error", message, **kwargs))
|
|
79
|
+
|
|
80
|
+
def debug(self, message: str, **kwargs: Any) -> None:
|
|
81
|
+
"""Log a debug message."""
|
|
82
|
+
if self.level >= LogLevel.VERBOSE:
|
|
83
|
+
self.console.print(self._format_message("debug", message, **kwargs))
|
|
84
|
+
|
|
85
|
+
def success(self, message: str, **kwargs: Any) -> None:
|
|
86
|
+
"""Log a success message."""
|
|
87
|
+
if self.level >= LogLevel.BASIC:
|
|
88
|
+
self.console.print(self._format_message("success", message, **kwargs))
|
|
89
|
+
|
|
90
|
+
def workflow_event(self, message: str, **kwargs: Any) -> None:
|
|
91
|
+
"""Log a workflow-specific event."""
|
|
92
|
+
if self.level >= LogLevel.BASIC:
|
|
93
|
+
self.console.print(self._format_message("workflow", message, **kwargs))
|
|
94
|
+
|
|
95
|
+
def activity_event(self, message: str, **kwargs: Any) -> None:
|
|
96
|
+
"""Log an activity-specific event."""
|
|
97
|
+
if self.level >= LogLevel.BASIC:
|
|
98
|
+
self.console.print(self._format_message("activity", message, **kwargs))
|
|
99
|
+
|
|
100
|
+
def result(self, message: dict, agent_name, **kwargs: Any) -> None:
|
|
101
|
+
"""Log a result message."""
|
|
102
|
+
if self.level >= LogLevel.MINIMAL:
|
|
103
|
+
AgentResultFormatter.print_result(message, agent_name, self.console)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Global logger instance with minimal logging by default
|
|
107
|
+
flock_logger = FlockLogger(level=LogLevel.MINIMAL)
|