Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions grimoire-runner/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies = [
"langchain-ollama>=0.1",
"wyrdbound-dice>=0.0.1",
"wyrdbound-rng>=0.0.1",
"jsonschema>=4.0",
]

[project.optional-dependencies]
Expand Down
7 changes: 7 additions & 0 deletions grimoire-runner/src/grimoire_runner/core/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
FlowDefinition,
InputDefinition,
LLMSettingsDefinition,
LLMValidationDefinition,
OutputDefinition,
StepDefinition,
StepType,
Expand Down Expand Up @@ -392,6 +393,11 @@ def _parse_step_definition(self, data: dict[str, Any]) -> StepDefinition:
if "llm_settings" in data:
llm_settings = LLMSettingsDefinition(**data["llm_settings"])

# Parse LLM validation
validation = None
if "validation" in data:
validation = LLMValidationDefinition(**data["validation"])

return StepDefinition(
id=data["id"],
name=data.get("name"), # Make name optional
Expand All @@ -415,6 +421,7 @@ def _parse_step_definition(self, data: dict[str, Any]) -> StepDefinition:
prompt_id=data.get("prompt_id"),
prompt_data=data.get("prompt_data", {}),
llm_settings=llm_settings,
validation=validation,
# Conditional step fields
if_condition=data.get("if"), # Map 'if' to 'if_condition'
then_actions=data.get("then"),
Expand Down
136 changes: 132 additions & 4 deletions grimoire-runner/src/grimoire_runner/executors/action_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ def execute(
resolved_value = value
logger.debug(f"Action set_value: Using boolean directly for {path}")
else:
# Resolve template in value for non-dict, non-boolean values
resolved_value = context.resolve_template(str(value))
logger.debug(f"Action set_value: Resolved template for {path}")
# Check if this is a variable assignment and if we can preserve object types
resolved_value = self._resolve_value_with_type_preservation(
value, path, context, system
)
logger.debug(f"Action set_value: Resolved value for {path}")

# Use namespaced paths to avoid collision during flow execution
current_namespace = context.get_current_flow_namespace()
Expand All @@ -83,6 +85,99 @@ def execute(
# Default to outputs
context.set_output(path, resolved_value)

def _resolve_value_with_type_preservation(
self,
value: Any,
path: str,
context: "ExecutionContext",
system: "System | None" = None,
) -> Any:
"""Resolve template value while preserving object types for typed variables."""
# Get the expected variable type if this is a variable assignment
expected_type = None
if path.startswith("variables.") and system:
variable_name = path[10:] # Remove "variables." prefix
expected_type = self._get_variable_type(variable_name, context, system)
logger.debug(f"Variable {variable_name} has expected type: {expected_type}")

logger.debug(
f"Resolving value for {path}: {repr(value)} (type: {type(value).__name__})"
)

# First resolve the template to get the actual value
resolved_value = context.resolve_template(str(value))
logger.debug(
f"Template resolved to: {repr(resolved_value)} (type: {type(resolved_value).__name__})"
)

# For roll_result type variables, try to preserve RollResult objects
if expected_type == "roll_result":
# Check if the resolved value is already a RollResult object
from ..models.roll_result import RollResult

if isinstance(resolved_value, RollResult):
logger.debug(f"Template resolved to RollResult object for {path}")
return resolved_value

# Post-processing: if we expected a roll_result but got a string, try to convert
if expected_type == "roll_result" and isinstance(resolved_value, str):
converted_result = self._convert_string_to_roll_result(resolved_value)
if converted_result:
logger.debug(f"Converted string to RollResult for {path}")
return converted_result

return resolved_value

def _get_variable_type(
self,
variable_name: str,
context: "ExecutionContext",
system: "System | None" = None,
) -> str | None:
"""Get the expected type for a variable from the flow definition."""
if not system:
return None

# Try to get the current flow definition to find variable type
current_execution = context.get_current_execution()
if current_execution and hasattr(current_execution, "flow_id"):
flow_id = current_execution.flow_id
if flow_id in system.flows:
flow_def = system.flows[flow_id]
for var_def in flow_def.variables:
if var_def.id == variable_name:
return var_def.type
return None

def _convert_string_to_roll_result(self, value_str: str) -> Any:
"""Try to convert a string back to a RollResult object if it looks like one."""
from ..models.roll_result import RollResult

# This is a simple heuristic - in practice, once we preserve the object properly,
# this shouldn't be needed, but it's here as a fallback
if not isinstance(value_str, str):
return None

# Look for patterns like "15 = 15 (1d20: 15) + 0" which indicate a dice roll result
import re

pattern = r"^(\d+)\s*=.*\(1d\d+.*\).*$"
match = re.match(pattern, value_str.strip())

if match:
try:
total = int(match.group(1))
# Create a basic RollResult from the parsed information
return RollResult(
total=total,
detail=value_str,
expression="unknown", # We'd need more parsing to get this
)
except (ValueError, AttributeError):
pass

return None


class DisplayValueActionStrategy(ActionStrategy):
"""Strategy for handling display_value actions."""
Expand Down Expand Up @@ -123,7 +218,39 @@ def execute(
"""Execute a log_event action."""
event_type = action_data.get("type", "unknown")
event_data = action_data.get("data", {})
logger.debug(f"Event: {event_type} - {event_data}")

# Resolve templates in event_data if it's a string
if isinstance(event_data, str):
resolved_event_data = context.resolve_template(event_data)
else:
resolved_event_data = event_data

logger.debug(f"Event: {event_type} - {resolved_event_data}")


class LogMessageActionStrategy(ActionStrategy):
"""Strategy for handling log_message actions."""

def get_action_type(self) -> str:
return "log_message"

def execute(
self,
action_data: dict[str, Any],
context: "ExecutionContext",
system: "System | None" = None,
) -> None:
"""Execute a log_message action."""
message = action_data.get("message", "")

# Resolve templates in the message
resolved_message = context.resolve_template(str(message))

# Add the message to the execution context for UI display
context.add_action_message(f"📝 {resolved_message}")

# Also log it for debugging
logger.debug(f"Action log_message: {resolved_message}")


class SwapValuesActionStrategy(ActionStrategy):
Expand Down Expand Up @@ -302,6 +429,7 @@ def _register_default_strategies(self) -> None:
SetValueActionStrategy(),
DisplayValueActionStrategy(),
LogEventActionStrategy(),
LogMessageActionStrategy(),
SwapValuesActionStrategy(),
FlowCallActionStrategy(self.table_executor_factory),
GetValueActionStrategy(),
Expand Down
20 changes: 15 additions & 5 deletions grimoire-runner/src/grimoire_runner/executors/dice_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, Any

from ..integrations.dice_integration import DiceIntegration
from ..models.roll_result import RollResult
from .base import BaseStepExecutor

if TYPE_CHECKING:
Expand Down Expand Up @@ -73,12 +74,21 @@ def _execute_dice_roll(
else:
logger.debug(f"Dice roll: {roll_expression} = {result.total}")

# Prepare result data
# Create structured RollResult object
roll_result = RollResult(
total=result.total,
detail=result.detailed_result or f"{result.total}",
expression=result.expression,
breakdown=result.breakdown,
individual_rolls=result.rolls,
)

# Prepare result data with both the structured object and legacy fields
result_data = {
"result": result.total,
"expression": roll_expression,
"breakdown": result.breakdown if hasattr(result, "breakdown") else None,
"individual_rolls": result.rolls if hasattr(result, "rolls") else None,
"result": roll_result, # New structured result
"expression": result.expression,
"breakdown": result.breakdown,
"individual_rolls": result.rolls,
}

return StepResult(
Expand Down
Loading