diff --git a/examples b/examples index 8279319f5..1656232ed 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 8279319f5509a46ad04c053ffe5bd0c9fbd482d8 +Subproject commit 1656232ed7face05ed8916cb14f1bed78ffcc70b diff --git a/pyproject.toml b/pyproject.toml index 285e3dfe7..3f468de95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flock-core" -version = "0.4.519" +version = "0.4.525" description = "Declarative LLM Orchestration at Scale" readme = "README.md" authors = [ @@ -45,7 +45,7 @@ dependencies = [ "thefuzz>=0.22.1", "tiktoken>=0.8.0", "toml>=0.10.2", - "tqdm>=4.67.1", + "tqdm>=4.60.1", "uvicorn>=0.34.0", "aiosqlite>=0.21.0", "markdown2>=2.5.3", @@ -54,15 +54,15 @@ dependencies = [ "opik>=1.7.26", "azure-data-tables>=12.7.0", "croniter>=6.0.0", - ] [project.optional-dependencies] basic-tools = [ - "docling>=2.18.0", + "docling>=2.34.0", "tavily-python>=0.5.0", "markdownify>=0.14.1", "duckduckgo-search>=7.3.2", + ] azure-tools = [ "azure-identity>=1.23.0", @@ -83,7 +83,7 @@ evaluation = [ "sentence-transformers>=3.4.1", ] all-tools = [ - "docling>=2.18.0", + "docling>=2.34.0", "tavily-python>=0.5.0", "markdownify>=0.14.1", "duckduckgo-search>=7.3.2", @@ -94,7 +94,7 @@ all-tools = [ "docker>=7.1.0", ] all = [ - "docling>=2.18.0", + "docling>=2.34.0", "tavily-python>=0.5.0", "markdownify>=0.14.1", "duckduckgo-search>=7.3.2", @@ -214,3 +214,11 @@ docs = ["_docs-build", "_docs-serve"] [tool.poe.tasks.clean] script = "poethepoet.scripts:rm('dist', 'htmlcov', 'logs','metrics','.mypy_cache', '.pytest_cache', './**/__pycache__')" + +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +explicit = true \ No newline at end of file diff --git a/src/flock/core/flock_factory.py b/src/flock/core/flock_factory.py index 43971bf6c..a3aa59547 100644 --- a/src/flock/core/flock_factory.py +++ b/src/flock/core/flock_factory.py @@ -413,6 +413,7 @@ def create_default_agent( write_to_file: bool = False, stream: bool = False, include_thought_process: bool = False, + include_reasoning: bool = False, temporal_activity_config: TemporalActivityConfig | None = None, ) -> FlockAgent: """Creates a default FlockAgent. @@ -433,6 +434,7 @@ def create_default_agent( max_retries=max_retries, stream=stream, include_thought_process=include_thought_process, + include_reasoning=include_reasoning, ) evaluator = DeclarativeEvaluator(name="default", config=eval_config) diff --git a/src/flock/evaluators/declarative/declarative_evaluator.py b/src/flock/evaluators/declarative/declarative_evaluator.py index 1e89dd21c..ef624eb65 100644 --- a/src/flock/evaluators/declarative/declarative_evaluator.py +++ b/src/flock/evaluators/declarative/declarative_evaluator.py @@ -36,6 +36,10 @@ class DeclarativeEvaluatorConfig(FlockEvaluatorConfig): default=False, description="Include the thought process in the output.", ) + include_reasoning: bool = Field( + default=False, + description="Include the reasoning in the output.", + ) kwargs: dict[str, Any] = Field(default_factory=dict) @@ -154,6 +158,9 @@ async def evaluate( self._lm_history = lm_history console.print("\n") + result_dict = self.filter_reasoning( + result_dict, self.config.include_reasoning + ) return self.filter_thought_process( result_dict, self.config.include_thought_process ) @@ -170,6 +177,9 @@ async def evaluate( ) self._cost = cost self._lm_history = lm_history + result_dict = self.filter_reasoning( + result_dict, self.config.include_reasoning + ) return self.filter_thought_process( result_dict, self.config.include_thought_process ) @@ -190,5 +200,18 @@ def filter_thought_process( return { k: v for k, v in result_dict.items() - if not (k.startswith("reasoning") or k.startswith("trajectory")) + if not (k.startswith("trajectory")) + } + + def filter_reasoning( + self, result_dict: dict[str, Any], include_reasoning: bool + ) -> dict[str, Any]: + """Filter out reasoning from the result dictionary.""" + if include_reasoning: + return result_dict + else: + return { + k: v + for k, v in result_dict.items() + if not (k.startswith("reasoning")) } diff --git a/src/flock/routers/conditional/conditional_router.py b/src/flock/routers/conditional/conditional_router.py index a07a85545..bf70c9c90 100644 --- a/src/flock/routers/conditional/conditional_router.py +++ b/src/flock/routers/conditional/conditional_router.py @@ -115,6 +115,10 @@ class ConditionalRouterConfig(FlockRouterConfig): default="flock.assertion_feedback", # Useful if paired with AssertionCheckerModule description="Optional context key containing feedback message to potentially include when retrying.", ) + feedback_on_failure: str | None = Field( + default=None, + description="Default feedback message to use when condition evaluation fails.", + ) retry_count_context_key_prefix: str = Field( default="flock.conditional_retry_count_", description="Internal prefix for context key storing retry attempts per agent.", diff --git a/src/flock/webapp/app/api/execution.py b/src/flock/webapp/app/api/execution.py index 7008b7978..2389d627c 100644 --- a/src/flock/webapp/app/api/execution.py +++ b/src/flock/webapp/app/api/execution.py @@ -10,6 +10,7 @@ Form, Request, ) +from fastapi.encoders import jsonable_encoder from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates @@ -151,7 +152,13 @@ async def htmx_run_flock( return HTMLResponse(f"
Error processing inputs for {start_agent_name}: {e_parse}
") result_data = await run_current_flock_service(start_agent_name, inputs, request.app.state) - raw_json_for_template = json.dumps(result_data, indent=2) + + + raw_json_for_template = json.dumps( + jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc. + indent=2, + ensure_ascii=False + ) # Unescape newlines for proper display in HTML tag
result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
root_path = request.scope.get("root_path", "")
@@ -215,7 +222,11 @@ async def htmx_run_shared_flock(
shared_logger.info(f"HTMX Run Shared: Executing agent '{start_agent_name}' in pre-loaded Flock '{temp_flock.name}'. Inputs: {list(inputs.keys())}")
result_data = await temp_flock.run_async(start_agent=start_agent_name, input=inputs, box_result=False)
- raw_json_for_template = json.dumps(result_data, indent=2)
+ raw_json_for_template = json.dumps(
+ jsonable_encoder(result_data), # ← converts every nested BaseModel, datetime, etc.
+ indent=2,
+ ensure_ascii=False
+ )
# Unescape newlines for proper display in HTML tag
result_data_raw_json_str = raw_json_for_template.replace('\\n', '\n')
shared_logger.info(f"HTMX Run Shared: Agent '{start_agent_name}' executed. Result keys: {list(result_data.keys()) if isinstance(result_data, dict) else 'N/A'}")
diff --git a/src/flock/webapp/app/services/sharing_store.py b/src/flock/webapp/app/services/sharing_store.py
index 412624979..f9d311c3d 100644
--- a/src/flock/webapp/app/services/sharing_store.py
+++ b/src/flock/webapp/app/services/sharing_store.py
@@ -4,6 +4,7 @@
import sqlite3
from abc import ABC, abstractmethod
from pathlib import Path
+from typing import Any
import aiosqlite
@@ -228,135 +229,181 @@ async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
logger.error(f"SQLite error saving feedback {record.feedback_id}: {e}", exc_info=True)
raise
+# ---------------------------------------------------------------------------
+# Azure Table + Blob implementation
+# ---------------------------------------------------------------------------
+
+try:
+ from azure.storage.blob.aio import BlobServiceClient
+ AZURE_BLOB_AVAILABLE = True
+except ImportError: # blob SDK not installed
+ AZURE_BLOB_AVAILABLE = False
+ BlobServiceClient = None
+
class AzureTableSharedLinkStore(SharedLinkStoreInterface):
- """Azure Table Storage implementation for storing and retrieving shared link configurations."""
+ """Store configs in Azure Table; store large flock YAML in Blob Storage."""
+
+ _TABLE_NAME = "flocksharedlinks"
+ _FEEDBACK_TBL_NAME = "flockfeedback"
+ _CONTAINER_NAME = "flocksharedlinkdefs" # blobs live here
+ _PARTITION_KEY = "shared_links"
def __init__(self, connection_string: str):
- """Initialize Azure Table Storage store with connection string."""
if not AZURE_AVAILABLE:
- raise ImportError("Azure Table Storage dependencies not available. Install with: pip install azure-data-tables")
+ raise ImportError("pip install azure-data-tables")
+ if not AZURE_BLOB_AVAILABLE:
+ raise ImportError("pip install azure-storage-blob")
self.connection_string = connection_string
- self.table_service_client = TableServiceClient.from_connection_string(connection_string)
- self.shared_links_table_name = "flocksharedlinks"
- self.feedback_table_name = "flockfeedback"
- logger.info("AzureTableSharedLinkStore initialized")
+ self.table_svc = TableServiceClient.from_connection_string(connection_string)
+ self.blob_svc = BlobServiceClient.from_connection_string(connection_string)
+ # ------------------------------------------------------------------ init
async def initialize(self) -> None:
- """Initializes the Azure Tables (creates them if they don't exist)."""
+ # 1. Azure Tables ----------------------------------------------------
try:
- # Create shared_links table
- try:
- await self.table_service_client.create_table(self.shared_links_table_name)
- logger.info(f"Created Azure Table: {self.shared_links_table_name}")
- except ResourceExistsError:
- logger.debug(f"Azure Table already exists: {self.shared_links_table_name}")
-
- # Create feedback table
- try:
- await self.table_service_client.create_table(self.feedback_table_name)
- logger.info(f"Created Azure Table: {self.feedback_table_name}")
- except ResourceExistsError:
- logger.debug(f"Azure Table already exists: {self.feedback_table_name}")
-
- logger.info("Azure Table Storage initialized successfully")
- except Exception as e:
- logger.error(f"Error initializing Azure Table Storage: {e}", exc_info=True)
- raise
+ await self.table_svc.create_table(self._TABLE_NAME)
+ logger.info("Created Azure Table '%s'", self._TABLE_NAME)
+ except ResourceExistsError:
+ logger.debug("Azure Table '%s' already exists", self._TABLE_NAME)
- async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
- """Saves a shared link configuration to Azure Table Storage."""
try:
- table_client = self.table_service_client.get_table_client(self.shared_links_table_name)
-
- entity = {
- "PartitionKey": "shared_links", # Use a fixed partition key for simplicity
- "RowKey": config.share_id,
- "share_id": config.share_id,
- "agent_name": config.agent_name,
- "flock_definition": config.flock_definition,
- "created_at": config.created_at.isoformat(),
- "share_type": config.share_type,
- "chat_message_key": config.chat_message_key,
- "chat_history_key": config.chat_history_key,
- "chat_response_key": config.chat_response_key,
- }
-
- await table_client.upsert_entity(entity)
- logger.info(f"Saved shared link config to Azure Table Storage for ID: {config.share_id} with type: {config.share_type}")
- return config
- except Exception as e:
- logger.error(f"Error saving config to Azure Table Storage for ID {config.share_id}: {e}", exc_info=True)
- raise
+ await self.table_svc.create_table(self._FEEDBACK_TBL_NAME)
+ logger.info("Created Azure Table '%s'", self._FEEDBACK_TBL_NAME)
+ except ResourceExistsError:
+ logger.debug("Azure Table '%s' already exists", self._FEEDBACK_TBL_NAME)
+
+ # 2. Blob container --------------------------------------------------
+ try:
+ await self.blob_svc.create_container(self._CONTAINER_NAME)
+ logger.info("Created Blob container '%s'", self._CONTAINER_NAME)
+ except ResourceExistsError:
+ logger.debug("Blob container '%s' already exists", self._CONTAINER_NAME)
+ # ------------------------------------------------------------- save_config
+ async def save_config(self, config: SharedLinkConfig) -> SharedLinkConfig:
+ """Upload YAML to Blob, then upsert table row containing the blob name."""
+ blob_name = f"{config.share_id}.yaml"
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+
+ # 1. Upload flock_definition (overwrite in case of retry)
+ await blob_client.upload_blob(config.flock_definition,
+ overwrite=True,
+ content_type="text/yaml")
+ logger.debug("Uploaded blob '%s' (%d bytes)",
+ blob_name, len(config.flock_definition.encode()))
+
+ # 2. Persist lightweight record in the table
+ tbl_client = self.table_svc.get_table_client(self._TABLE_NAME)
+ entity = {
+ "PartitionKey": self._PARTITION_KEY,
+ "RowKey": config.share_id,
+ "agent_name": config.agent_name,
+ "created_at": config.created_at.isoformat(),
+ "share_type": config.share_type,
+ "chat_message_key": config.chat_message_key,
+ "chat_history_key": config.chat_history_key,
+ "chat_response_key": config.chat_response_key,
+ # NEW – just a few bytes, well under 64 KiB
+ "flock_blob_name": blob_name,
+ }
+ await tbl_client.upsert_entity(entity)
+ logger.info("Saved shared link %s → blob '%s'", config.share_id, blob_name)
+ return config
+
+ # -------------------------------------------------------------- get_config
async def get_config(self, share_id: str) -> SharedLinkConfig | None:
- """Retrieves a shared link configuration from Azure Table Storage by its ID."""
+ tbl_client = self.table_svc.get_table_client(self._TABLE_NAME)
try:
- table_client = self.table_service_client.get_table_client(self.shared_links_table_name)
-
- entity = await table_client.get_entity(partition_key="shared_links", row_key=share_id)
-
- logger.debug(f"Retrieved shared link config from Azure Table Storage for ID: {share_id}")
- return SharedLinkConfig(
- share_id=entity["share_id"],
- agent_name=entity["agent_name"],
- created_at=entity["created_at"], # Pydantic will parse from ISO format
- flock_definition=entity["flock_definition"],
- share_type=entity.get("share_type", "agent_run"),
- chat_message_key=entity.get("chat_message_key"),
- chat_history_key=entity.get("chat_history_key"),
- chat_response_key=entity.get("chat_response_key"),
- )
+ entity = await tbl_client.get_entity(self._PARTITION_KEY, share_id)
except ResourceNotFoundError:
- logger.debug(f"No shared link config found in Azure Table Storage for ID: {share_id}")
+ logger.debug("No config entity for id '%s'", share_id)
return None
+
+ blob_name = entity["flock_blob_name"]
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+ try:
+ blob_bytes = await (await blob_client.download_blob()).readall()
+ flock_yaml = blob_bytes.decode()
except Exception as e:
- logger.error(f"Error retrieving config from Azure Table Storage for ID {share_id}: {e}", exc_info=True)
- return None
+ logger.error("Cannot download blob '%s' for share_id=%s: %s",
+ blob_name, share_id, e, exc_info=True)
+ raise
+ return SharedLinkConfig(
+ share_id = share_id,
+ agent_name = entity["agent_name"],
+ created_at = entity["created_at"],
+ flock_definition = flock_yaml,
+ share_type = entity.get("share_type", "agent_run"),
+ chat_message_key = entity.get("chat_message_key"),
+ chat_history_key = entity.get("chat_history_key"),
+ chat_response_key = entity.get("chat_response_key"),
+ )
+
+ # ----------------------------------------------------------- delete_config
async def delete_config(self, share_id: str) -> bool:
- """Deletes a shared link configuration from Azure Table Storage by its ID."""
+ tbl_client = self.table_svc.get_table_client(self._TABLE_NAME)
try:
- table_client = self.table_service_client.get_table_client(self.shared_links_table_name)
-
- await table_client.delete_entity(partition_key="shared_links", row_key=share_id)
- logger.info(f"Deleted shared link config from Azure Table Storage for ID: {share_id}")
- return True
+ entity = await tbl_client.get_entity(self._PARTITION_KEY, share_id)
except ResourceNotFoundError:
- logger.info(f"Attempted to delete non-existent shared link config from Azure Table Storage for ID: {share_id}")
- return False
- except Exception as e:
- logger.error(f"Error deleting config from Azure Table Storage for ID {share_id}: {e}", exc_info=True)
+ logger.info("Delete: entity %s not found", share_id)
return False
- # ----------------------- Feedback methods -----------------------
+ # 1. Remove blob (ignore missing blob)
+ blob_name = entity["flock_blob_name"]
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+ try:
+ await blob_client.delete_blob(delete_snapshots="include")
+ logger.debug("Deleted blob '%s'", blob_name)
+ except ResourceNotFoundError:
+ logger.warning("Blob '%s' already gone", blob_name)
+
+ # 2. Remove table row
+ await tbl_client.delete_entity(self._PARTITION_KEY, share_id)
+ logger.info("Deleted shared link %s and its blob", share_id)
+ return True
+ # -------------------------------------------------------- save_feedback --
async def save_feedback(self, record: FeedbackRecord) -> FeedbackRecord:
- """Persist a feedback record to Azure Table Storage."""
- try:
- table_client = self.table_service_client.get_table_client(self.feedback_table_name)
-
- entity = {
- "PartitionKey": "feedback", # Use a fixed partition key for simplicity
- "RowKey": record.feedback_id,
- "feedback_id": record.feedback_id,
- "share_id": record.share_id,
- "context_type": record.context_type,
- "reason": record.reason,
- "expected_response": record.expected_response,
- "actual_response": record.actual_response,
- "flock_name": record.flock_name,
- "agent_name": record.agent_name,
- "flock_definition": record.flock_definition,
- "created_at": record.created_at.isoformat(),
- }
-
- await table_client.upsert_entity(entity)
- logger.info(f"Saved feedback to Azure Table Storage: {record.feedback_id} (share={record.share_id})")
- return record
- except Exception as e:
- logger.error(f"Error saving feedback to Azure Table Storage {record.feedback_id}: {e}", exc_info=True)
- raise
+ """Persist a feedback record. If a flock_definition is present, upload it as a blob and
+ store only a reference in the table row to avoid oversized entities (64 KiB limit).
+ """
+ tbl_client = self.table_svc.get_table_client(self._FEEDBACK_TBL_NAME)
+
+ # Core entity fields (avoid dumping the full Pydantic model – too many columns / large value)
+ entity: dict[str, Any] = {
+ "PartitionKey": "feedback",
+ "RowKey": record.feedback_id,
+ "share_id": record.share_id,
+ "context_type": record.context_type,
+ "reason": record.reason,
+ "expected_response": record.expected_response,
+ "actual_response": record.actual_response,
+ "created_at": record.created_at.isoformat(),
+ }
+ if record.flock_name is not None:
+ entity["flock_name"] = record.flock_name
+ if record.agent_name is not None:
+ entity["agent_name"] = record.agent_name
+
+ # ------------------------------------------------------------------ YAML → Blob
+ if record.flock_definition:
+ blob_name = f"{record.feedback_id}.yaml"
+ blob_client = self.blob_svc.get_blob_client(self._CONTAINER_NAME, blob_name)
+ # Overwrite=true so repeated feedback_id uploads (shouldn't happen) won't error
+ await blob_client.upload_blob(record.flock_definition,
+ overwrite=True,
+ content_type="text/yaml")
+ entity["flock_blob_name"] = blob_name # lightweight reference only
+
+ # ------------------------------------------------------------------ Table upsert
+ await tbl_client.upsert_entity(entity)
+ logger.info("Saved feedback %s%s",
+ record.feedback_id,
+ f" → blob '{entity['flock_blob_name']}'" if "flock_blob_name" in entity else "")
+ return record
+
# ----------------------- Factory Function -----------------------
diff --git a/uv.lock b/uv.lock
index d0b4d9c29..7dd3736f5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1005,7 +1005,7 @@ wheels = [
[[package]]
name = "docling"
-version = "2.33.0"
+version = "2.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
@@ -1035,9 +1035,9 @@ dependencies = [
{ name = "tqdm" },
{ name = "typer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/36/d8/76740f7d40a28794365e76029e7379f7d1b5994e24aaac899c3bb280e426/docling-2.33.0.tar.gz", hash = "sha256:40c27df6a7f90b8fb4279a4094ac26c48f3dbd38ceda0d3208e6d63eda2e8660", size = 132120 }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/42/33334eeaa3d42a4b3406adc7bbef8c655d026c1c41ad0a8fa858bbb7ef1b/docling-2.34.0.tar.gz", hash = "sha256:52ad3c79b7e56e978fcdd8f2040fe739584e25e1c2fd5f9519fe3d51002de0d2", size = 135016 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9c/23/6dddf454610c4dff5799f9becbbc752b8595169de57c02a1d39067f5f2b5/docling-2.33.0-py3-none-any.whl", hash = "sha256:6c0ee223c5da551adc5da15aae205aaebf78222780198cb5a21c85648da618ba", size = 169441 },
+ { url = "https://files.pythonhosted.org/packages/be/b6/b855f19ab37a6f92b60a31a7db57160048f88a980aef4b97a5c260860ed4/docling-2.34.0-py3-none-any.whl", hash = "sha256:a69c368382cc824a5a5ad21882b8772f85117a25c81257a57d54582bf38b2810", size = 173153 },
]
[[package]]
@@ -1282,7 +1282,7 @@ wheels = [
[[package]]
name = "flock-core"
-version = "0.4.518"
+version = "0.4.524"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -1426,9 +1426,9 @@ requires-dist = [
{ name = "devtools", specifier = ">=0.12.2" },
{ name = "docker", marker = "extra == 'all-tools'", specifier = ">=7.1.0" },
{ name = "docker", marker = "extra == 'code-tools'", specifier = ">=7.1.0" },
- { name = "docling", marker = "extra == 'all'", specifier = ">=2.18.0" },
- { name = "docling", marker = "extra == 'all-tools'", specifier = ">=2.18.0" },
- { name = "docling", marker = "extra == 'basic-tools'", specifier = ">=2.18.0" },
+ { name = "docling", marker = "extra == 'all'", specifier = ">=2.34.0" },
+ { name = "docling", marker = "extra == 'all-tools'", specifier = ">=2.34.0" },
+ { name = "docling", marker = "extra == 'basic-tools'", specifier = ">=2.34.0" },
{ name = "dspy", specifier = "==2.6.23" },
{ name = "duckduckgo-search", marker = "extra == 'all'", specifier = ">=7.3.2" },
{ name = "duckduckgo-search", marker = "extra == 'all-tools'", specifier = ">=7.3.2" },
@@ -1481,7 +1481,7 @@ requires-dist = [
{ name = "thefuzz", specifier = ">=0.22.1" },
{ name = "tiktoken", specifier = ">=0.8.0" },
{ name = "toml", specifier = ">=0.10.2" },
- { name = "tqdm", specifier = ">=4.67.1" },
+ { name = "tqdm", specifier = ">=4.60.1" },
{ name = "uvicorn", specifier = ">=0.34.0" },
{ name = "wd-di", specifier = ">=0.2.14" },
{ name = "websockets", specifier = ">=15.0.1" },