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
25 changes: 24 additions & 1 deletion inc/Abilities/Engine/ExecuteStepAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ private function routeAfterExecution(
array(
'flow_step_id' => $flow_step_id,
'class' => $step_class,
'reason' => 'empty_data_packet_returned',
'reason' => $this->getFailureReasonFromPackets( $dataPackets, 'empty_data_packet_returned' ),
)
);

Expand All @@ -531,4 +531,27 @@ private function routeAfterExecution(
'outcome' => 'failed',
);
}

/**
* Extract failure reason from step packets.
*
* @param array $dataPackets Data packets from step execution.
* @param string $default Default reason when none found.
* @return string
*/
private function getFailureReasonFromPackets( array $dataPackets, string $default ): string {
foreach ( $dataPackets as $packet ) {
$metadata = $packet['metadata'] ?? array();
if ( empty( $metadata['failure_reason'] ) ) {
continue;
}

$reason = $metadata['failure_reason'];
if ( is_string( $reason ) && '' !== trim( $reason ) ) {
return sanitize_key( str_replace( ' ', '_', trim( $reason ) ) );
}
}

return $default;
}
}
201 changes: 193 additions & 8 deletions inc/Core/Steps/Update/UpdateStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,105 @@ class: self::class,
* @return array
*/
protected function executeStep(): array {
$handler = $this->getHandlerSlug();
$configured_handler_slugs = $this->getHandlerSlugs();
$required_handler_slugs = $this->getRequiredHandlerSlugs( $configured_handler_slugs );
$tool_results_by_slug = $this->findSuccessfulHandlerResultsBySlug( $required_handler_slugs );

$missing_required_handlers = array_values( array_diff( $required_handler_slugs, array_keys( $tool_results_by_slug ) ) );

if ( empty( $missing_required_handlers ) && ! empty( $required_handler_slugs ) ) {
$primary_handler_slug = $required_handler_slugs[0];
$tool_result_entry = $tool_results_by_slug[ $primary_handler_slug ] ?? null;

if ( ! is_array( $tool_result_entry ) ) {
$this->log(
'error',
'Update step missing primary tool result despite required handlers being satisfied',
array(
'primary_handler_slug' => $primary_handler_slug,
)
);

return $this->buildMissingHandlerPacket(
$configured_handler_slugs,
$required_handler_slugs,
array( $primary_handler_slug )
);
}

$tool_result_entry = ToolResultFinder::findHandlerResult( $this->dataPackets, $handler, $this->flow_step_id );
if ( $tool_result_entry ) {
$this->log(
'info',
'AI successfully used handler tool',
'AI successfully executed required update handler tools',
array(
'handler' => $handler,
'tool_result' => $tool_result_entry['metadata']['tool_name'] ?? 'unknown',
'primary_handler' => $primary_handler_slug,
'required_handlers' => $required_handler_slugs,
)
);

return $this->create_update_entry_from_tool_result( $tool_result_entry, $this->dataPackets, $handler, $this->flow_step_id );
return $this->create_update_entry_from_tool_result( $tool_result_entry, $this->dataPackets, $primary_handler_slug, $this->flow_step_id );
}

return array();
$this->log(
'warning',
'Update step required handler tool was not executed by AI',
array(
'configured_handlers' => $configured_handler_slugs,
'required_handler_slugs' => $required_handler_slugs,
'missing_required_handlers' => $missing_required_handlers,
)
);

return $this->buildMissingHandlerPacket( $configured_handler_slugs, $required_handler_slugs, $missing_required_handlers );
}

/**
* Validate update step configuration.
*
* @return bool
*/
protected function validateStepConfiguration(): bool {
$configured_handler_slugs = $this->getHandlerSlugs();

if ( empty( $configured_handler_slugs ) ) {
$this->logConfigurationError(
'Step requires handler configuration',
array(
'available_flow_step_config' => array_keys( $this->flow_step_config ),
)
);
return false;
}

$raw_required_handlers = $this->getRawRequiredHandlerSlugs();

if ( empty( $raw_required_handlers ) && count( $configured_handler_slugs ) > 1 ) {
$this->log(
'warning',
'Multi-handler update step has no required_handler_slugs set; defaulting to first handler',
array(
'configured_handlers' => $configured_handler_slugs,
'default_required' => array( $configured_handler_slugs[0] ),
)
);
}

if ( ! empty( $raw_required_handlers ) ) {
$invalid_handlers = array_values( array_diff( $raw_required_handlers, $configured_handler_slugs ) );

if ( ! empty( $invalid_handlers ) ) {
$this->logConfigurationError(
'required_handler_slugs must be a subset of handler_slugs',
array(
'configured_handlers' => $configured_handler_slugs,
'required_handlers' => $raw_required_handlers,
'invalid_handlers' => $invalid_handlers,
)
);
return false;
}
}

return true;
}

/**
Expand Down Expand Up @@ -114,4 +196,107 @@ private function create_update_entry_from_tool_result( array $tool_result_entry,

return $packet->addTo( $dataPackets );
}

/**
* Build failure packet when required handlers were not called.
*
* @param array $configured_handler_slugs Configured handler slugs.
* @param array $required_handler_slugs Required handler slugs.
* @param array $missing_required_handlers Missing required handlers.
* @return array
*/
private function buildMissingHandlerPacket( array $configured_handler_slugs, array $required_handler_slugs, array $missing_required_handlers ): array {
$packet = new DataPacket(
array(
'update_result' => array(),
'updated_at' => current_time( 'mysql', true ),
),
array(
'step_type' => 'update',
'handler' => $required_handler_slugs[0] ?? ( $configured_handler_slugs[0] ?? '' ),
'flow_step_id' => $this->flow_step_id,
'success' => false,
'failure_reason' => 'required_handler_tool_not_called',
'missing_handler_tool' => true,
'configured_handler_slugs' => $configured_handler_slugs,
'required_handler_slugs' => $required_handler_slugs,
'missing_required_handlers' => $missing_required_handlers,
),
'update'
);

return $packet->addTo( $this->dataPackets );
}

/**
* Resolve required handler slugs for this update step.
*
* @param array $configured_handler_slugs Configured handler slugs.
* @return array
*/
private function getRequiredHandlerSlugs( array $configured_handler_slugs ): array {
$required = $this->getRawRequiredHandlerSlugs();

if ( ! empty( $required ) ) {
return $required;
}

if ( empty( $configured_handler_slugs ) ) {
return array();
}

return array( $configured_handler_slugs[0] );
}

/**
* Get required handler slugs from flow step config.
*
* @return array
*/
private function getRawRequiredHandlerSlugs(): array {
$required = $this->flow_step_config['required_handler_slugs'] ?? array();

if ( ! is_array( $required ) ) {
return array();
}

$required = array_values(
array_unique(
array_filter(
array_map(
static function ( $slug ) {
if ( ! is_string( $slug ) ) {
return '';
}

return sanitize_key( $slug );
},
$required
)
)
)
);

return $required;
}

/**
* Find successful tool results keyed by handler slug.
*
* @param array $handler_slugs Handler slugs to search for.
* @return array<string, array>
*/
private function findSuccessfulHandlerResultsBySlug( array $handler_slugs ): array {
$results = array();

foreach ( $handler_slugs as $handler_slug ) {
$entry = ToolResultFinder::findHandlerResult( $this->dataPackets, $handler_slug, $this->flow_step_id, false );

if ( is_array( $entry ) ) {
$results[ $handler_slug ] = $entry;
}
}

return $results;
}
}
24 changes: 13 additions & 11 deletions inc/Engine/AI/Tools/ToolResultFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ class ToolResultFinder {
* @param array $dataPackets Data packet array from pipeline execution
* @param string $handler Handler slug to match
* @param string $flow_step_id Flow step ID for error logging context
* @param bool $log_error_on_missing Whether to log an error when no match is found.
* @return array|null Tool result entry or null if no match found
*/
public static function findHandlerResult( array $dataPackets, string $handler, string $flow_step_id ): ?array {
public static function findHandlerResult( array $dataPackets, string $handler, string $flow_step_id, bool $log_error_on_missing = true ): ?array {
foreach ( $dataPackets as $entry ) {
$entry_type = $entry['type'] ?? '';

Expand All @@ -53,16 +54,17 @@ public static function findHandlerResult( array $dataPackets, string $handler, s
}
}

// Log error when not found
do_action(
'datamachine_log',
'error',
'AI did not execute handler tool',
array(
'handler' => $handler,
'flow_step_id' => $flow_step_id,
)
);
if ( $log_error_on_missing ) {
do_action(
'datamachine_log',
'error',
'AI did not execute handler tool',
array(
'handler' => $handler,
'flow_step_id' => $flow_step_id,
)
);
}

return null;
}
Expand Down
61 changes: 61 additions & 0 deletions tests/Unit/AI/Tools/ToolResultFinderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* ToolResultFinder unit tests.
*
* @package DataMachine\Tests\Unit\AI\Tools
*/

namespace DataMachine\Tests\Unit\AI\Tools;

use DataMachine\Engine\AI\Tools\ToolResultFinder;
use PHPUnit\Framework\TestCase;

class ToolResultFinderTest extends TestCase {

public function test_find_handler_result_logs_error_by_default_when_missing(): void {
$logged = array();

add_action(
'datamachine_log',
function ( $level, $message, $context ) use ( &$logged ) {
$logged[] = array(
'level' => $level,
'message' => $message,
'context' => $context,
);
},
10,
3
);

$result = ToolResultFinder::findHandlerResult( array(), 'upsert_event', 'flow_step_1' );

$this->assertNull( $result );
$this->assertNotEmpty( $logged );
$this->assertSame( 'error', $logged[0]['level'] );
$this->assertSame( 'AI did not execute handler tool', $logged[0]['message'] );
$this->assertSame( 'upsert_event', $logged[0]['context']['handler'] );
}

public function test_find_handler_result_can_skip_error_logging_when_missing(): void {
$logged = array();

add_action(
'datamachine_log',
function ( $level, $message, $context ) use ( &$logged ) {
$logged[] = array(
'level' => $level,
'message' => $message,
'context' => $context,
);
},
10,
3
);

$result = ToolResultFinder::findHandlerResult( array(), 'upsert_event', 'flow_step_1', false );

$this->assertNull( $result );
$this->assertSame( array(), $logged );
}
}
Loading
Loading