Claude 24f816b6a3
Consolidate 22 sibling repos into layered organism structure
Place useful parts of the surrounding repos into sica-fondt by layer, per the
body model (Ada = membrane; brain/endocrine/capabilities/knowledge non-Ada):

- brain/        LLM reasoning + providers (dapr, hermes, MoMoA)
- capabilities/ REPRAG sidecars: hermes tools/skills, dapr tools, parallel
                dispatch, A51 channels, and the OSINT cluster
- knowledge/    LORAG corpus: 754 cyber-skills, agency personas, secure-coding,
                MITRE ATT&CK data
- reference/    defensive threat-reference (C3, shhbruh doc) + AdaYaml parser

License handling: AGPL sources (worldosint, advanced_evolution, mercury,
Reticulum) and GPL DeTTECT are SPEC-only clean-room/port descriptions — no
copyleft code copied. MIT/Apache/data parts copied as working trees.

Safety: shhbruh escape/persistence material and C3 covert-C2 kept as reference
only, not wired into the running organism. See CONSOLIDATION.md.

https://claude.ai/code/session_01UehUqEXXJJCsHoA4voCU5c
2026-06-10 06:53:01 +00:00

534 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#
# Copyright 2026 The Dapr Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import logging
import os
import time
from pathlib import Path
from typing import (
Any,
ClassVar,
Dict,
Iterable,
List,
Literal,
Optional,
Type,
Union,
)
from pydantic import BaseModel, Field
from dapr_agents.llm.chat import ChatClientBase
from dapr_agents.llm.dapr.client import DaprInferenceClientBase
from dapr_agents.llm.utils import RequestHandler, ResponseHandler
from dapr_agents.prompt.base import PromptTemplateBase
from dapr_agents.prompt.prompty import Prompty
from dapr_agents.tool import AgentTool
from dapr_agents.types.exceptions import DaprRuntimeVersionNotSupportedError
from dapr_agents.types.message import (
BaseMessage,
LLMChatResponse,
)
from dapr_agents.utils import is_version_supported
# Lazy import to avoid import issues during test collection
def _import_conversation_types():
from dapr.clients.grpc.conversation import (
ConversationInputAlpha2,
ConversationMessage,
ConversationMessageOfAssistant,
ConversationMessageContent,
ConversationToolCalls,
ConversationToolCallsOfFunction,
create_user_message,
create_system_message,
create_assistant_message,
create_tool_message,
)
return (
ConversationInputAlpha2,
ConversationMessage,
ConversationMessageOfAssistant,
ConversationMessageContent,
ConversationToolCalls,
ConversationToolCallsOfFunction,
create_user_message,
create_system_message,
create_assistant_message,
create_tool_message,
)
logger = logging.getLogger(__name__)
class DaprChatClient(DaprInferenceClientBase, ChatClientBase):
"""
Chat client for Dapr's Inference API.
Integrates Prompty-driven prompt templates, tool injection,
PII scrubbing, and normalizes the Dapr output into our unified
LLMChatResponse schema. **Streaming is not supported.**
"""
prompty: Optional[Prompty] = Field(
default=None, description="Optional Prompty instance for templating."
)
prompt_template: Optional[PromptTemplateBase] = Field(
default=None, description="Optional prompt-template to format inputs."
)
component_name: Optional[str] = None
# Support both function_call and json structured output modes
SUPPORTED_STRUCTURED_MODES: ClassVar[set[str]] = {"function_call", "json"}
def model_post_init(self, __context: Any) -> None:
"""
After Pydantic init, set up API/type and default LLM component from env.
"""
self._api = "chat"
self._llm_component = self.component_name
if not self._llm_component:
self._llm_component = os.environ.get("DAPR_LLM_COMPONENT_DEFAULT")
if not self._llm_component:
logger.debug(
"No LLM component provided and no default component found in the environment. Will try to get it from the metadata at runtime."
)
super().model_post_init(__context)
@classmethod
def from_prompty(
cls,
prompty_source: Union[str, Path],
timeout: Union[int, float, Dict[str, Any]] = 1500,
) -> "DaprChatClient":
"""
Build a DaprChatClient from a Prompty spec.
Args:
prompty_source: Path or inline Prompty YAML/JSON.
timeout: Request timeout in seconds or HTTPX-style dict.
Returns:
Configured DaprChatClient.
"""
prompty_instance = Prompty.load(prompty_source)
prompt_template = Prompty.to_prompt_template(prompty_instance)
return cls.model_validate(
{
"timeout": timeout,
"prompty": prompty_instance,
"prompt_template": prompt_template,
}
)
def translate_response(self, response: dict, model: str) -> dict:
"""
Convert Dapr Alpha2 response into OpenAI-style ChatCompletion dict.
"""
if not isinstance(response, dict):
logger.error(f"Invalid response type: {type(response)}")
raise ValueError(f"Response must be a dictionary, got {type(response)}")
# Flatten all output choices from Alpha2 envelope
choices: List[Dict[str, Any]] = []
outputs = response.get("outputs", []) or []
if not isinstance(outputs, list):
logger.error(f"Invalid outputs type: {type(outputs)}")
raise ValueError(f"Outputs must be a list, got {type(outputs)}")
for output in outputs:
if not isinstance(output, dict):
logger.error(f"Invalid output type: {type(output)}")
continue
output_choices = output.get("choices", []) or []
if not isinstance(output_choices, list):
logger.error(f"Invalid choices type: {type(output_choices)}")
continue
for choice in output_choices:
if not isinstance(choice, dict):
logger.error(f"Invalid choice type: {type(choice)}")
continue
# Ensure message is present and has required fields
message = choice.get("message", {})
if not isinstance(message, dict):
logger.error(f"Invalid message type: {type(message)}")
continue
# Add required fields if missing
if "content" not in message:
message["content"] = ""
if "role" not in message:
message["role"] = "assistant"
choice["message"] = message
choices.append(choice)
return {
"choices": choices,
"created": int(time.time()),
"model": model,
"object": "chat.completion",
"usage": {"total_tokens": "-1"},
}
def convert_to_conversation_inputs(self, inputs: List[Dict[str, Any]]) -> List[Any]:
"""
Map normalized messages into a single Alpha2 ConversationInput that preserves history.
Alpha2 expects a list of ConversationMessage entries inside one ConversationInputAlpha2
for a turn. If there are tool results, they must reference prior assistant tool_calls by id.
"""
# Lazy import conversation types
(
ConversationInputAlpha2,
ConversationMessage,
ConversationMessageOfAssistant,
ConversationMessageContent,
ConversationToolCalls,
ConversationToolCallsOfFunction,
create_user_message,
create_system_message,
create_assistant_message,
create_tool_message,
) = _import_conversation_types()
history_messages: List[ConversationMessage] = []
scrub_flags: List[bool] = []
for item in inputs:
role = item.get("role")
content = item.get("content", "")
if role == "user":
msg = create_user_message(content)
elif role == "system":
msg = create_system_message(content)
elif role == "assistant":
# Preserve assistant tool_calls if present (OpenAI-like schema)
tool_calls_data = item.get("tool_calls") or []
if tool_calls_data:
converted_calls: List[ConversationToolCalls] = []
for tc in tool_calls_data:
fn = tc.get("function", {}) if isinstance(tc, dict) else {}
name = fn.get("name", "")
arguments = fn.get("arguments", "")
# Ensure arguments is a string
if not isinstance(arguments, str):
try:
import json as _json
arguments = _json.dumps(arguments)
except Exception:
arguments = str(arguments)
converted_calls.append(
ConversationToolCalls(
id=tc.get("id", None),
function=ConversationToolCallsOfFunction(
name=name, arguments=arguments
),
)
)
msg = ConversationMessage(
of_assistant=ConversationMessageOfAssistant(
content=[ConversationMessageContent(text=content)],
tool_calls=converted_calls,
)
)
else:
msg = create_assistant_message(content)
elif role == "tool":
tool_id = item.get("tool_call_id") or item.get("id") or ""
name = item.get("name", "")
msg = create_tool_message(tool_id, name, content)
else:
raise ValueError(f"Unsupported role for Alpha2 conversion: {role}")
history_messages.append(msg)
scrub_flags.append(bool(item.get("scrubPII")))
# Use scrub_pii if any message requested it
scrub_any = any(scrub_flags) if scrub_flags else None
return [ConversationInputAlpha2(messages=history_messages, scrub_pii=scrub_any)]
def generate(
self,
messages: Union[
str,
Dict[str, Any],
BaseMessage,
Iterable[Union[Dict[str, Any], BaseMessage]],
] = None,
*,
input_data: Optional[Dict[str, Any]] = None,
llm_component: Optional[str] = None,
tools: Optional[List[Union[AgentTool, Dict[str, Any]]]] = None,
response_format: Optional[Type[BaseModel]] = None,
structured_mode: Literal["function_call", "json"] = "json",
scrubPII: bool = False,
temperature: Optional[float] = None,
**kwargs: Any,
) -> Union[
LLMChatResponse,
BaseModel,
List[BaseModel],
]:
"""
Issue a non-streaming chat completion via Dapr.
- **Streaming is not supported** and setting `stream=True` will raise.
- Returns a unified `LLMChatResponse` (if no `response_format`), or
validated Pydantic model(s) when `response_format` is provided.
Args:
messages: Prebuilt messages or None to use `input_data`.
input_data: Variables for Prompty template rendering.
llm_component: Dapr component name (defaults from env).
tools: AgentTool or dict specifications.
response_format: Pydantic model for structured output.
structured_mode: "json" (default) or "function_call".
scrubPII: Obfuscate sensitive output if True.
temperature: Sampling temperature.
**kwargs: Other Dapr API parameters.
Returns:
• `LLMChatResponse` if no `response_format`
• Pydantic model (or `List[...]`) when `response_format` is set
Raises:
ValueError: on invalid `structured_mode`, missing inputs, or if `stream=True`.
"""
# 1) Validate structured_mode
if structured_mode not in self.SUPPORTED_STRUCTURED_MODES:
raise ValueError(
f"structured_mode must be one of {self.SUPPORTED_STRUCTURED_MODES}"
)
# 2) Disallow streaming
# Note: response_format is now supported for structured output
if kwargs.get("stream"):
raise ValueError("Streaming is not supported by DaprChatClient.")
# 3) Build messages via Prompty
if input_data:
if not self.prompt_template:
raise ValueError("input_data provided but no prompt_template is set.")
messages = self.prompt_template.format_prompt(**input_data)
if not messages:
raise ValueError("Either 'messages' or 'input_data' must be provided.")
# 4) Normalize + merge defaults
params: Dict[str, Any] = {
"inputs": RequestHandler.normalize_chat_messages(messages)
}
if self.prompty:
params = {**self.prompty.model.parameters.model_dump(), **params, **kwargs}
else:
params.update(kwargs)
# 5) Inject tools + structured directives
params = RequestHandler.process_params(
params,
llm_provider=self.provider,
tools=tools,
response_format=response_format,
structured_mode=structured_mode,
)
logger.debug(f"Processed parameters for Dapr: {params}")
if response_format:
logger.debug(f"Response format: {response_format}")
logger.debug(f"Structured mode: {structured_mode}")
# 6) Convert to Dapr inputs & call
conv_inputs = self.convert_to_conversation_inputs(params["inputs"])
try:
logger.debug("Invoking the Dapr Conversation API.")
# Log tools/tool_choice/parameters for debugging
if params.get("tools"):
try:
logger.debug(
f"Alpha2 tools payload: {[t.get('function', {}).get('name', '') for t in params['tools'] if isinstance(t, dict)]}"
)
except Exception:
logger.warning(
"Alpha2 tools payload present (could not render names)."
)
if params.get("tool_choice") is not None:
logger.debug(f"Alpha2 tool_choice: {params.get('tool_choice')}")
if params.get("parameters") is not None:
logger.debug(
f"Alpha2 parameters keys: {list(params.get('parameters', {}).keys())}"
)
# get metadata information from the dapr client
metadata = self.client.get_metadata()
_check_dapr_runtime_support(metadata)
llm_component = llm_component or self._llm_component
if not llm_component:
llm_component = _get_llm_component(metadata)
# Extract additional API parameters (response_format is sent via the
# dedicated response_format field, not duplicated here)
api_params = {}
if "structured_mode" in params:
api_params["structured_mode"] = str(params["structured_mode"])
# Convert tool_choice from dict format to string of 'none', 'auto', or 'required'
tool_choice_param = params.get("tool_choice")
if isinstance(tool_choice_param, dict):
# When a specific function is requested (dict format), use 'required'
# to force tool calling, since Dapr doesn't support function-specific selection
tool_choice_param = "required"
elif isinstance(tool_choice_param, str) and tool_choice_param not in (
"none",
"auto",
"required",
):
# If it's a string but not a valid Dapr value (e.g., a function name),
# default to 'required' to force tool calling
logger.warning(
f"tool_choice value '{tool_choice_param}' is not supported by Dapr. "
"Using 'required' instead."
)
tool_choice_param = "required"
raw = self.client.chat_completion_alpha2(
llm=llm_component or self._llm_component,
inputs=conv_inputs,
scrub_pii=scrubPII,
temperature=temperature,
tools=params.get("tools"),
tool_choice=tool_choice_param,
parameters=api_params or None,
response_format=_to_dapr_response_format(params.get("response_format")),
)
normalized = self.translate_response(
raw, llm_component or self._llm_component
)
logger.debug(f"Dapr Conversation API response: {raw}")
logger.debug(f"Normalized response: {normalized}")
except Exception as e:
logger.warning(f"Dapr Conversation API call failed: {e}")
raise
# 7) Hand off to our unified handler (always nonstream)
return ResponseHandler.process_response(
response=normalized,
llm_provider=self.provider,
response_format=response_format,
structured_mode=structured_mode,
stream=False,
)
def _simplify_anyof(schema: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively collapse ``anyOf`` unions into a single ``type`` field.
The Dapr runtime's ``convertToStructuredOutputSchema`` (Go) requires every
sub-schema to have a ``"type"`` string. Pydantic emits
``anyOf: [{"type": "X"}, {"type": "null"}]`` for ``Optional[X]`` fields,
which has *no* top-level ``"type"`` and causes a conversion error that
crashes the sidecar (nil-logger panic on the error path).
This helper rewrites such patterns to ``{"type": "X"}`` (dropping the
null variant) so the Go code can process the schema safely.
"""
result = dict(schema)
# Collapse anyOf: [{"type": T}, {"type": "null"}] → {"type": T}
if "anyOf" in result and "type" not in result:
variants = result.get("anyOf", [])
non_null = [
v for v in variants if isinstance(v, dict) and v.get("type") != "null"
]
if len(non_null) == 1:
collapsed = dict(non_null[0])
# Preserve sibling keys (description, title, default, etc.)
for k, v in result.items():
if k != "anyOf" and k not in collapsed:
collapsed[k] = v
result = collapsed
# Recurse into object properties
if result.get("type") == "object" and "properties" in result:
result["properties"] = {
k: _simplify_anyof(v) if isinstance(v, dict) else v
for k, v in result["properties"].items()
}
# Recurse into array items
if result.get("type") == "array" and isinstance(result.get("items"), dict):
result["items"] = _simplify_anyof(result["items"])
return result
def _to_dapr_response_format(
oai_format: Optional[Dict[str, Any]],
) -> Optional[Dict[str, Any]]:
"""
Convert an OpenAI-style response_format dict to the flat JSON schema format
expected by the Dapr runtime's convertToStructuredOutputDefinition.
OAI format: {"type": "json_schema", "json_schema": {"name": ..., "description": ..., "strict": ..., "schema": {<json-schema>}}}
Dapr format: {<json-schema fields>, "name": ..., "description": ..., "strict": ...}
"""
if oai_format is None:
return None
if oai_format.get("type") == "json_schema":
inner = oai_format.get("json_schema", {})
schema = inner.get("schema", {})
simplified = _simplify_anyof(schema)
return {
**simplified,
"name": inner.get("name", "response"),
"description": inner.get("description", ""),
"strict": inner.get("strict", False),
}
return oai_format
def _check_dapr_runtime_support(metadata: "GetMetadataResponse"): # noqa: F821
"""Check if the Dapr runtime version is supported for Alpha2 Chat Client."""
extended_metadata = metadata.extended_metadata
dapr_runtime_version = extended_metadata.get("daprRuntimeVersion", None)
if dapr_runtime_version is not None:
# Allow only versions >=1.16.0, edge, and <2.0.0 for Alpha2 Chat Client
if not is_version_supported(str(dapr_runtime_version), ">=1.16.0, <2.0.0"):
raise DaprRuntimeVersionNotSupportedError(
f"!!!!! Dapr Runtime Version {dapr_runtime_version} is not supported with Alpha2 Dapr Chat Client. Only Dapr runtime versions >=1.16.0 and <2.0.0 are supported."
)
def _get_llm_component(metadata: "GetMetadataResponse") -> str: # noqa: F821
"""Get the LLM component from the metadata."""
conversation_components = [
component
for component in metadata.registered_components
if component.type.startswith("conversation.")
]
if len(conversation_components) == 1:
return conversation_components[0].name
elif len(conversation_components) > 1:
raise ValueError(
"Multiple LLM components found in the metadata. Please provide the component name explicitly (e.g. llm = DaprChatClient(component_name='openai')) or environment variable DAPR_LLM_COMPONENT_DEFAULT."
)
else:
raise ValueError(
"No LLM component provided and no default component found in the metadata."
)