From 0384376511068fb87425efa99c2ed1d4e2118924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Fri, 20 Mar 2026 11:57:41 +0200 Subject: [PATCH 1/2] Agent Connector 4.3.0: Add sema4_api_url param, batch work items, Snowflake support - Add sema4_api_url Secret to all actions for explicit API URL configuration - Add create_work_items_for_agent() for batch Work Item creation - Add get_current_conversation_id() to expose thread ID to worker agents - Add Snowflake deployment support with URL normalisation and token auth - Remove work_item_api_url param from create_work_item_for_agent() (derived from sema4_api_url) - Refactor auth header logic into _get_auth_header() helper - Remove debug prints and raise statement left in request() method Co-Authored-By: Claude Sonnet 4.6 --- actions/agent-connector/CHANGELOG.md | 17 ++ actions/agent-connector/actions.py | 230 ++++++++++++++------ actions/agent-connector/agent_api_client.py | 71 ++++-- actions/agent-connector/package.yaml | 2 +- 4 files changed, 232 insertions(+), 88 deletions(-) diff --git a/actions/agent-connector/CHANGELOG.md b/actions/agent-connector/CHANGELOG.md index 9ef3682e..0bdcf3b0 100644 --- a/actions/agent-connector/CHANGELOG.md +++ b/actions/agent-connector/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). +### [4.3.0] - 2026-03-20 + +### Added + +- New `sema4_api_url` Secret parameter to all actions, enabling explicit API URL configuration +- New `create_work_items_for_agent()` action for batch-creating multiple Work Items for an agent in one call +- New `get_current_conversation_id()` action to retrieve the current thread/conversation ID for passing to worker agents +- Snowflake deployment support: URL normalization and `Snowflake Token` auth header for `snowflakecomputing` endpoints +- Auto-detection fallback: when `SEMA4AI_API_V1_URL` env var is set, explicit URL is ignored in favor of auto-detection +- Better JSON parse error reporting with status code and response body on failed API responses + +### Changed + +- `_AgentAPIClient` now accepts an optional `api_url` parameter alongside `api_key` +- `create_work_item_for_agent()` derives Work Item API URL from `sema4_api_url` instead of a separate `work_item_api_url` parameter (removed) +- Auth header logic extracted to `_get_auth_header()` helper, used consistently across all request paths + ### [4.2.1] - 2026-02-10 ### Changed diff --git a/actions/agent-connector/actions.py b/actions/agent-connector/actions.py index 8d290917..298cda48 100644 --- a/actions/agent-connector/actions.py +++ b/actions/agent-connector/actions.py @@ -1,3 +1,5 @@ +import json + from pydantic import BaseModel, ConfigDict from sema4ai.actions import ( ActionError, @@ -6,8 +8,10 @@ action, ) +from sema4ai.actions.agent import get_thread_id from agent_api_client import _AgentAPIClient, Agent + class AgentResult(BaseModel): found: bool agent: Agent | None = None @@ -16,38 +20,43 @@ class AgentResult(BaseModel): suggested_name: str | None = None message: str | None = None -def find_closest_match(target: str, candidates: list[str], threshold: float = 0.6) -> str | None: + +def find_closest_match( + target: str, candidates: list[str], threshold: float = 0.6 +) -> str | None: """Find the closest matching string from a list of candidates. - + Args: target: The string to find a match for candidates: List of candidate strings threshold: Minimum similarity score (0-1) to consider a match - + Returns: The closest matching string or None if no match above threshold """ if not candidates: return None - + target_lower = target.lower() best_match = None best_score = 0 - + for candidate in candidates: candidate_lower = candidate.lower() - + # Exact match if target_lower == candidate_lower: return candidate - + # Check if target is contained in candidate or vice versa if target_lower in candidate_lower or candidate_lower in target_lower: - score = min(len(target_lower), len(candidate_lower)) / max(len(target_lower), len(candidate_lower)) + score = min(len(target_lower), len(candidate_lower)) / max( + len(target_lower), len(candidate_lower) + ) if score > best_score: best_score = score best_match = candidate - + # Check for common prefix/suffix common_prefix = 0 for i in range(min(len(target_lower), len(candidate_lower))): @@ -55,49 +64,51 @@ def find_closest_match(target: str, candidates: list[str], threshold: float = 0. common_prefix += 1 else: break - + if common_prefix > 0: score = common_prefix / max(len(target_lower), len(candidate_lower)) if score > best_score: best_score = score best_match = candidate - + return best_match if best_score >= threshold else None def resolve_agent_by_name(client: _AgentAPIClient, agent_name: str) -> AgentResult: """Centralized function to resolve an agent by name and provide suggestions if not found. - + Args: client: The agent API client instance agent_name: The name of the agent to find - + Returns: AgentResult with found=True and agent populated if found, or found=False with suggestions if not found """ # First, try to find the agent agent = client.get_agent_by_name(agent_name) if agent: - return AgentResult( - found=True, - agent=agent - ) - + return AgentResult(found=True, agent=agent) + # Agent not found, get all agents to provide suggestions all_agents = client.get_all_agents() available_names = [agent.name for agent in all_agents] - + # Find the closest matching name suggested_name = find_closest_match(agent_name, available_names, threshold=0.6) - + return AgentResult( found=False, requested_name=agent_name, available_agent_names=available_names, suggested_name=suggested_name, - message=f"Agent '{agent_name}' not found. Available agents: {', '.join(available_names)}" + message=f"Agent '{agent_name}' not found. Available agents: {', '.join(available_names)}", ) + +def _make_client(sema4_api_key: Secret, sema4_api_url: Secret) -> _AgentAPIClient: + return _AgentAPIClient(api_key=sema4_api_key.value, api_url=sema4_api_url.value) + + class Conversation(BaseModel): id: str name: str @@ -121,120 +132,127 @@ class WorkItemPayload(BaseModel): model_config = ConfigDict(extra="allow") + @action def ask_agent( - agent_name: str, - message: str, + agent_name: str, + message: str, sema4_api_key: Secret, + sema4_api_url: Secret, conversation_id: str | None = None, - conversation_name: str | None = None + conversation_name: str | None = None, ) -> Response[MessageResponse]: """The simplest way to ask an agent a question, by name. Creates a new conversation if conversation_id is not provided. Args: agent_name: The name of the agent to send message to message: The message content to send + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! conversation_id: Optional conversation ID. If not provided, a new conversation will be created. Provide a conversation ID to send a message to an existing conversation for follow-up and to maintain context. conversation_name: Optional name for the new conversation (only used if conversation_id is not provided) - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! Returns: Response containing the conversation ID and agent's response """ - client = _AgentAPIClient(api_key=sema4_api_key.value) - + client = _make_client(sema4_api_key, sema4_api_url) + # First, find the agent by name agent_result = resolve_agent_by_name(client, agent_name) if not agent_result.found: raise ActionError(agent_result.message) - + agent = agent_result.agent - + # If no conversation_id provided, create a new conversation if not conversation_id: if not conversation_name: conversation_name = f"Conversation with {agent_name}" - + conversation = client.create_conversation( - agent_id=agent.id, - conversation_name=conversation_name + agent_id=agent.id, conversation_name=conversation_name ) conversation_id = conversation.id - + # Send the message response = client.send_message( conversation_id=conversation_id, agent_id=agent.id, message=message, ) - - return Response(result=MessageResponse( - conversation_id=conversation_id, - response=response, - agent_name=agent_name - )) + + return Response( + result=MessageResponse( + conversation_id=conversation_id, response=response, agent_name=agent_name + ) + ) + @action -def get_all_agents(sema4_api_key: Secret) -> Response[list[Agent]]: +def get_all_agents(sema4_api_key: Secret, sema4_api_url: Secret) -> Response[list[Agent]]: """Only use this to get a list of all available agents. If you're asking an agent by name, use ask_agent instead. Fetches a list of all available agents with their IDs and names. Args: - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: Response containing either a JSON string of agents or an error message """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) return Response(result=client.get_all_agents()) @action -def get_agent_by_name(name: str, sema4_api_key: Secret) -> Response[AgentResult]: - """Only use this to resolve an agent by name. If you're asking an agent by name, use ask_agent instead. If the agent is not found, returns a result with available agent names and suggestions. +def get_agent_by_name(name: str, sema4_api_key: Secret, sema4_api_url: Secret) -> Response[AgentResult]: + """Only use this to resolve an agent by name. If you're asking an agent by name, use ask_agent instead. If the agent is not found, returns a result with available agent names and suggestions. Args: name: The name of the agent - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: Response containing an AgentResult with either the found agent or suggestions for available agents """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) return Response(result=resolve_agent_by_name(client, name)) @action def get_conversations( - agent_id: str, sema4_api_key: Secret + agent_id: str, sema4_api_key: Secret, sema4_api_url: Secret ) -> Response[list[Conversation]]: """Fetches all conversations for an agent. Args: agent_id: The ID of the agent - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: Response containing either a JSON string of conversations or an error message """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) return Response(result=client.get_conversations(agent_id)) @action def get_conversation( - agent_name: str, conversation_name: str, sema4_api_key: Secret + agent_name: str, conversation_name: str, sema4_api_key: Secret, sema4_api_url: Secret ) -> Response[Conversation]: """Fetches a conversation for an agent. Args: agent_name: The name of the agent conversation_name: The name of the conversation - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: Response containing either the conversation ID or an error message """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) conversation = client.get_conversation( agent_name=agent_name, conversation_name=conversation_name ) @@ -248,19 +266,20 @@ def get_conversation( @action def get_conversation_messages( - agent_id: str, conversation_id: str, sema4_api_key: Secret + agent_id: str, conversation_id: str, sema4_api_key: Secret, sema4_api_url: Secret ) -> Response[list]: """Fetches all messages from a specific conversation. Args: agent_id: The ID of the agent conversation_id: The ID of the conversation - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: Response containing either a JSON string of the conversation with messages or an error message """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) messages = client.get_conversation_messages( agent_id=agent_id, conversation_id=conversation_id ) @@ -270,19 +289,20 @@ def get_conversation_messages( @action def create_conversation( - agent_id: str, conversation_name: str, sema4_api_key: Secret + agent_id: str, conversation_name: str, sema4_api_key: Secret, sema4_api_url: Secret ) -> Response[Conversation]: """Creates a new conversation for communication with an agent. Args: agent_id: The id of the agent to create conversation with conversation_name: The name of the conversation to be created - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: The created conversation object. """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) return Response( result=client.create_conversation( agent_id=agent_id, conversation_name=conversation_name @@ -292,7 +312,7 @@ def create_conversation( @action def send_message( - conversation_id: str, agent_id: str, message: str, sema4_api_key: Secret + conversation_id: str, agent_id: str, message: str, sema4_api_key: Secret, sema4_api_url: Secret ) -> Response[str]: """Sends a message within a conversation and retrieves the agent's response. @@ -300,12 +320,13 @@ def send_message( conversation_id: The ID of the conversation agent_id: The ID of the agent to send message to message: The message content to send - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! Returns: Response containing either the agent's response or an error message """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) response = client.send_message( conversation_id=conversation_id, agent_id=agent_id, @@ -315,37 +336,114 @@ def send_message( return Response(result=response) +@action +def get_current_conversation_id() -> Response[str]: + """Returns the conversation ID of the current agent run. + Call this to obtain the conversation_id to include in work item payloads + so that worker agents can send notifications back to this conversation. + + Returns: + Response containing the current conversation ID (thread_id) + """ + + return Response(result=get_thread_id()) + + +@action +def create_work_items_for_agent( + agent_name: str, + work_items_json: str, + sema4_api_key: Secret, + sema4_api_url: Secret, +) -> Response[list[WorkItemResponse]]: + """Creates one Work Item per entry for a specific agent. + Each work item has its own message, payload, and file attachments. + The message is merged into the payload so the worker receives it as payload["message"]. + + Args: + agent_name: The name of the agent to run the Work Items + work_items_json: JSON array string. Each element must have: + - "message" (str): short description of the work item + - "payload" (dict): variables to pass to the worker, e.g. {"filename": "x.pdf", "batch_id": "...", ...} + - "attachments" (list[str], optional): file paths to attach, e.g. ["x.pdf"] + Example: [{"message": "Process x.pdf", "payload": {"filename": "x.pdf", "batch_id": "abc"}, "attachments": ["x.pdf"]}] + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! + + Returns: + Response containing a list of created Work Item details, one per work item + """ + try: + work_items = json.loads(work_items_json) + except json.JSONDecodeError as e: + raise ActionError(f"work_items_json is not valid JSON: {e}") from e + + if not isinstance(work_items, list): + raise ActionError("work_items_json must be a JSON array") + + client = _make_client(sema4_api_key, sema4_api_url) + agent_result = resolve_agent_by_name(client, agent_name) + if not agent_result.found: + raise ActionError(agent_result.message) + + wi_url = sema4_api_url.value if sema4_api_url.value.upper() != "LOCAL" else None + + agent = agent_result.agent + results = [] + for item in work_items: + message = item.get("message", "") + payload = item.get("payload", {}) + attachments = item.get("attachments") + full_payload = {"message": message, **payload} + work_item = client.create_work_item( + agent_id=agent.id, + payload=full_payload, + attachments=attachments, + work_item_api_url=wi_url, + ) + results.append( + WorkItemResponse( + work_item=work_item, + agent_name=agent.name, + agent_id=agent.id, + ) + ) + return Response(result=results) + + @action def create_work_item_for_agent( agent_name: str, payload: WorkItemPayload, sema4_api_key: Secret, + sema4_api_url: Secret, attachments: list[str] | None = None, - work_item_api_url: str | None = None, ) -> Response[WorkItemResponse]: """Creates a Work Item for a specific agent by name. Args: agent_name: The name of the agent to run the Work Item payload: JSON payload to send as the Work Item payload (any properties allowed) - sema4_api_key: The API key for the Sema4 API if running in cloud. Use LOCAL if in Studio or SDK! + sema4_api_key: The API key for the Sema4 API. Use LOCAL if in Studio or SDK! + sema4_api_url: The base URL for the Sema4 API. Use LOCAL if in Studio or SDK! attachments: Optional list of file paths to attach to the Work Item - work_item_api_url: Optional Work Item API URL override Returns: Response containing the created Work Item details """ - client = _AgentAPIClient(api_key=sema4_api_key.value) + client = _make_client(sema4_api_key, sema4_api_url) agent_result = resolve_agent_by_name(client, agent_name) if not agent_result.found: raise ActionError(agent_result.message) + wi_url = sema4_api_url.value if sema4_api_url.value.upper() != "LOCAL" else None + agent = agent_result.agent work_item = client.create_work_item( agent_id=agent.id, payload=payload.model_dump(exclude_none=True), attachments=attachments, - work_item_api_url=work_item_api_url, + work_item_api_url=wi_url, ) return Response( result=WorkItemResponse( diff --git a/actions/agent-connector/agent_api_client.py b/actions/agent-connector/agent_api_client.py index c07cd9e6..42b5d31d 100644 --- a/actions/agent-connector/agent_api_client.py +++ b/actions/agent-connector/agent_api_client.py @@ -30,13 +30,45 @@ class AgentApiClientException(Exception): class _AgentAPIClient: PID_FILE_NAME = "agent-server.pid" - def __init__(self, api_key: str | None = None): + def __init__(self, api_key: str | None = None, api_url: str | None = None): """Initialize the AgentServerClient.""" - self.api_key = api_key if api_key != "LOCAL" else None - self.api_url = self._get_api_url() - self.is_v2 = "v2" in self.api_url - - print(f"API URL: {self.api_url}") + self.api_key = api_key if (api_key and api_key.upper() != "LOCAL") else None + resolved_url = api_url if (api_url and api_url.upper() != "LOCAL") else None + # When SEMA4AI_API_V1_URL is set (e.g. running inside Snowflake), the agent + # server is reachable via that internal URL. Ignore the explicit URL so that + # auto-detection picks it up instead of trying to reach the external hostname. + if resolved_url and os.getenv("SEMA4AI_API_V1_URL"): + print( + f"SEMA4AI_API_V1_URL env var detected — ignoring explicit URL '{resolved_url}', using auto-detect" + ) + resolved_url = None + if resolved_url: + self.api_url = self._normalize_api_url(resolved_url) + self.is_v2 = "v2" in self.api_url + print(f"API URL (explicit): {self.api_url}") + else: + self.api_url = self._get_api_url() + self.is_v2 = "v2" in self.api_url + print(f"API URL: {self.api_url}") + + def _normalize_api_url(self, url: str) -> str: + """Ensure the URL ends with /api/v1 for Snowflake deployments.""" + url = url.rstrip("/") + if "snowflakecomputing" in url: + if not url.endswith("/v1") and not url.endswith("/v2"): + if url.endswith("/api"): + url = f"{url}/v1" + elif not url.endswith("/api/v1") and "/api/" not in url: + url = f"{url}/api/v1" + return url + + def _get_auth_header(self) -> dict: + """Return the correct Authorization header based on deployment environment.""" + if not self.api_key: + return {} + if "snowflakecomputing" in getattr(self, "api_url", ""): + return {"Authorization": f'Snowflake Token="{self.api_key}"'} + return {"Authorization": f"Bearer {self.api_key}"} def _get_api_url(self) -> str: """Determine the correct API URL by checking environment variable or agent-server.pid file @@ -112,9 +144,7 @@ def _is_url_accessible(self, url: str) -> bool: if parsed_url.scheme not in ("http", "https"): return False - headers = ( - {"Authorization": f"Bearer {self.api_key}"} if self.api_key else None - ) + headers = self._get_auth_header() or None print(f"Testing URL: {url}") sema4ai_http.get(url, headers=headers, timeout=1).raise_for_status() return True @@ -188,9 +218,7 @@ def request( url += "/" request_headers = copy(headers) if headers else {} - - if self.api_key: - request_headers["Authorization"] = f"Bearer {self.api_key}" + request_headers.update(self._get_auth_header()) if method == "GET": response = sema4ai_http.get(url, json=json_data, headers=request_headers) @@ -227,8 +255,7 @@ def _request_with_base_url( url = urljoin(base_url, path) request_headers = copy(headers) if headers else {} - if self.api_key: - request_headers["Authorization"] = f"Bearer {self.api_key}" + request_headers.update(self._get_auth_header()) if method == "GET": response = sema4ai_http.get(url, json=json_data, headers=request_headers) @@ -298,9 +325,7 @@ def _post_work_item_upload( upload_url = urljoin(base_url.rstrip("/") + "/", "upload-file") response = sema4ai_http.post(upload_url, headers=headers, fields=fields) if response.status_code == 404 and fallback_url: - fallback_upload_url = urljoin( - fallback_url.rstrip("/") + "/", "upload-file" - ) + fallback_upload_url = urljoin(fallback_url.rstrip("/") + "/", "upload-file") response = sema4ai_http.post( fallback_upload_url, headers=headers, fields=fields ) @@ -374,7 +399,13 @@ def _get_all_pages(self, endpoint: str) -> list: paginated_endpoint = f"{endpoint}?next={next_token}" response = self.request(paginated_endpoint) - response_json = response.json() + try: + response_json = response.json() + except Exception as e: + raise AgentApiClientException( + f"Failed to parse response as JSON from {self.api_url}. " + f"Status: {response.status_code}, Body: {response.text!r}" + ) from e all_data.extend( response_json.get("data", response_json.get("messages", [])) @@ -541,9 +572,7 @@ def _upload_work_item_file( ) -> tuple[str, str]: filename = os.path.basename(file_path) content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" - request_headers = ( - {"Authorization": f"Bearer {self.api_key}"} if self.api_key else None - ) + request_headers = self._get_auth_header() or None if os.path.exists(file_path): with open(file_path, "rb") as file_handle: diff --git a/actions/agent-connector/package.yaml b/actions/agent-connector/package.yaml index 6ae307de..1dd2917e 100644 --- a/actions/agent-connector/package.yaml +++ b/actions/agent-connector/package.yaml @@ -5,7 +5,7 @@ name: Agent Connector description: Actions to connect agents with each other # Package version number, recommend using semver.org -version: 4.2.1 +version: 4.3.0 # The version of the `package.yaml` format. spec-version: v2 From 0acc122bcf6317d1cd08fec291c2d13095897b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=20Ha=CC=88nninen?= Date: Fri, 20 Mar 2026 12:01:01 +0200 Subject: [PATCH 2/2] Agent Connector: Update README for 4.3.0 changes - Document sema4_api_url Secret parameter and authentication table - Add Snowflake environment instructions - Add create_work_items_for_agent batch usage example - Add get_current_conversation_id usage example Co-Authored-By: Claude Sonnet 4.6 --- actions/agent-connector/README.md | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/actions/agent-connector/README.md b/actions/agent-connector/README.md index 82a9e81d..1d47d6af 100644 --- a/actions/agent-connector/README.md +++ b/actions/agent-connector/README.md @@ -29,6 +29,8 @@ The easiest way to interact with agents is using the `ask_agent` function: **Work Items:** - Create a Work Item for an agent by name +- Create multiple Work Items in a single batch call +- Get the current conversation ID (for passing back to a parent agent) ## Example Usage @@ -80,6 +82,28 @@ If an attachment path is not found locally, the action will try to fetch it from Create a work item for "Invoice Worker" with payload {"invoice_id": "IN-100017"} and attachments ["ad03f9489278c8e19d01ea5e05ee0aeb.pdf"] ``` +### Create Multiple Work Items in One Call + +Use `create_work_items_for_agent` to dispatch several items at once. Each entry can have its own message, payload, and attachments: + +``` +Create work items for "Invoice Agent": +[ + {"message": "Process invoice IN-001", "payload": {"invoice_id": "IN-001"}}, + {"message": "Process invoice IN-002", "payload": {"invoice_id": "IN-002"}, "attachments": ["IN-002.pdf"]} +] +``` + +The `message` field is automatically merged into the payload so the worker receives it as `payload["message"]`. + +### Pass the Current Conversation ID to Worker Agents + +When orchestrating workers, use `get_current_conversation_id` to retrieve the calling conversation's ID and include it in the work item payload so workers can send replies back: + +``` +Get the current conversation ID and create a work item for "Summarizer Agent" with payload {"conversation_id": "", "document": "report.pdf"} +``` + ### Intelligent Suggestions If you mistype an agent name, the system will suggest the closest match: @@ -109,10 +133,18 @@ For more control, you can also use the individual actions: 2. **Conversation Management**: Create conversations manually or retrieve existing ones 3. **Message Sending**: Send messages to specific conversations with full control over the process -### Authentication +### Authentication & API URL + +All actions require two Secret parameters: + +| Parameter | Description | +|---|---| +| `sema4_api_key` | API key for the Sema4 API. Use `LOCAL` when running in Studio or SDK. | +| `sema4_api_url` | Base URL for the Sema4 API. Use `LOCAL` when running in Studio or SDK. | -- **Cloud Environment**: Uses Bearer token authentication with your API key -- **Local Development**: Use "LOCAL" as the API key value (no authentication required) +- **Cloud Environment**: Provide your API key and the deployment base URL (e.g. `https://your-tenant.sema4.ai`). +- **Snowflake Environment**: Provide the Snowflake endpoint URL — the connector automatically uses `Snowflake Token` authentication and normalises the URL to end with `/api/v1`. +- **Local Development**: Use `LOCAL` for both values — the connector discovers the agent server automatically via environment variable or PID file. ### Intelligent Features