Skip to content

Commit c2cd230

Browse files
committed
update middleware
1 parent aa4cc36 commit c2cd230

15 files changed

+398
-533
lines changed

src/Agents/Agent.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
use Cortex\Agents\Contracts\AfterModelMiddleware;
5959
use Cortex\Agents\Contracts\BeforeModelMiddleware;
6060
use Cortex\Agents\Contracts\BeforePromptMiddleware;
61+
use Cortex\Agents\Middleware\BeforePromptWrapper;
62+
use Cortex\Agents\Middleware\BeforeModelWrapper;
63+
use Cortex\Agents\Middleware\AfterModelWrapper;
6164
use Cortex\Contracts\ChatMemory as ChatMemoryContract;
6265

6366
class Agent implements Pipeable
@@ -383,14 +386,53 @@ protected function executionStages(): array
383386

384387
/**
385388
* Get the middleware of a specific type.
389+
* If middleware implements multiple middleware interfaces, wrap it appropriately
390+
* to delegate to the correct hook method (beforePrompt, beforeModel, or afterModel).
386391
*
387392
* @param class-string<\Cortex\Agents\Contracts\Middleware> $type
388393
*
389394
* @return array<int, \Cortex\Agents\Contracts\Middleware>
390395
*/
391396
protected function getMiddleware(string $type): array
392397
{
393-
return array_filter($this->middleware, fn(Middleware $middleware): bool => $middleware instanceof $type);
398+
return array_map(
399+
function (Middleware $middleware) use ($type): Middleware {
400+
// Wrap all hook-based middleware to ensure hook methods are called
401+
if (! $this->isHookMiddlewareType($type)) {
402+
return $middleware;
403+
}
404+
405+
// If middleware implements multiple interfaces, wrap to delegate to correct hook
406+
// If it only implements one interface, still wrap to ensure hook method is called
407+
return $this->wrapMiddleware($middleware, $type);
408+
},
409+
array_filter($this->middleware, fn(Middleware $middleware): bool => $middleware instanceof $type)
410+
);
411+
}
412+
413+
/**
414+
* Check if the given type is a hook-based middleware interface.
415+
*/
416+
protected function isHookMiddlewareType(string $type): bool
417+
{
418+
return in_array($type, [
419+
BeforePromptMiddleware::class,
420+
BeforeModelMiddleware::class,
421+
AfterModelMiddleware::class,
422+
], true);
423+
}
424+
425+
/**
426+
* Wrap middleware to delegate to the appropriate hook method.
427+
*/
428+
protected function wrapMiddleware(Middleware $middleware, string $type): Middleware
429+
{
430+
return match ($type) {
431+
BeforePromptMiddleware::class => new BeforePromptWrapper($middleware),
432+
BeforeModelMiddleware::class => new BeforeModelWrapper($middleware),
433+
AfterModelMiddleware::class => new AfterModelWrapper($middleware),
434+
default => $middleware,
435+
};
394436
}
395437

396438
/**

src/Agents/Contracts/AfterModelMiddleware.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@
44

55
namespace Cortex\Agents\Contracts;
66

7+
use Closure;
8+
use Cortex\Pipeline\RuntimeConfig;
9+
710
/**
811
* Middleware that runs after the LLM model call.
912
*/
10-
interface AfterModelMiddleware extends Middleware {}
13+
interface AfterModelMiddleware extends Middleware
14+
{
15+
/**
16+
* Hook that runs after the model call.
17+
*
18+
* @param mixed $payload The input to process
19+
* @param RuntimeConfig $config The runtime context
20+
* @param Closure $next The next stage in the pipeline
21+
*
22+
* @return mixed The processed result
23+
*/
24+
public function afterModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed;
25+
}

src/Agents/Contracts/BeforeModelMiddleware.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@
44

55
namespace Cortex\Agents\Contracts;
66

7+
use Closure;
8+
use Cortex\Pipeline\RuntimeConfig;
9+
710
/**
811
* Middleware that runs before the LLM model call.
912
*/
10-
interface BeforeModelMiddleware extends Middleware {}
13+
interface BeforeModelMiddleware extends Middleware
14+
{
15+
/**
16+
* Hook that runs before the model call.
17+
*
18+
* @param mixed $payload The input to process
19+
* @param RuntimeConfig $config The runtime context
20+
* @param Closure $next The next stage in the pipeline
21+
*
22+
* @return mixed The processed result
23+
*/
24+
public function beforeModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed;
25+
}

src/Agents/Contracts/BeforePromptMiddleware.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@
44

55
namespace Cortex\Agents\Contracts;
66

7+
use Closure;
8+
use Cortex\Pipeline\RuntimeConfig;
9+
710
/**
811
* Middleware that runs before the prompt is processed.
912
*/
10-
interface BeforePromptMiddleware extends Middleware {}
13+
interface BeforePromptMiddleware extends Middleware
14+
{
15+
/**
16+
* Hook that runs before the prompt is processed.
17+
*
18+
* @param mixed $payload The input to process
19+
* @param RuntimeConfig $config The runtime context
20+
* @param Closure $next The next stage in the pipeline
21+
*
22+
* @return mixed The processed result
23+
*/
24+
public function beforePrompt(mixed $payload, RuntimeConfig $config, Closure $next): mixed;
25+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\Agents\Middleware;
6+
7+
use Closure;
8+
use Cortex\Pipeline\RuntimeConfig;
9+
use Cortex\Support\Traits\CanPipe;
10+
use Cortex\Agents\Contracts\Middleware;
11+
12+
/**
13+
* Abstract base class that provides default implementations for middleware hook methods.
14+
*
15+
* This class does not implement any middleware interfaces - consumers should implement
16+
* the interfaces they need (BeforePromptMiddleware, BeforeModelMiddleware, AfterModelMiddleware).
17+
* The hook methods provided here can be used to satisfy those interface requirements.
18+
*
19+
* Example - implementing a single interface:
20+
* ```php
21+
* class BeforePromptOnly extends AbstractMiddleware implements BeforePromptMiddleware
22+
* {
23+
* // beforePrompt() is provided by AbstractMiddleware, delegates to handlePipeable()
24+
* // Override if you need custom logic
25+
* }
26+
* ```
27+
*
28+
* Example - implementing multiple interfaces:
29+
* ```php
30+
* class MyMiddleware extends AbstractMiddleware
31+
* implements BeforePromptMiddleware, BeforeModelMiddleware, AfterModelMiddleware
32+
* {
33+
* public function beforePrompt(mixed $payload, RuntimeConfig $config, Closure $next): mixed
34+
* {
35+
* // Custom logic before prompt
36+
* return $next($payload, $config);
37+
* }
38+
*
39+
* public function beforeModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
40+
* {
41+
* // Custom logic before model call
42+
* return $next($payload, $config);
43+
* }
44+
*
45+
* // afterModel() uses default from AbstractMiddleware (delegates to handlePipeable())
46+
* }
47+
* ```
48+
*/
49+
abstract class AbstractMiddleware implements Middleware
50+
{
51+
use CanPipe;
52+
53+
/**
54+
* Hook that runs before the prompt is processed.
55+
* Default implementation delegates to handlePipeable().
56+
* Override this method to provide before-prompt logic.
57+
*
58+
* @param mixed $payload The input to process
59+
* @param RuntimeConfig $config The runtime context
60+
* @param Closure $next The next stage in the pipeline
61+
*
62+
* @return mixed The processed result
63+
*/
64+
public function beforePrompt(mixed $payload, RuntimeConfig $config, Closure $next): mixed
65+
{
66+
return $this->handlePipeable($payload, $config, $next);
67+
}
68+
69+
/**
70+
* Hook that runs before the model call.
71+
* Default implementation delegates to handlePipeable().
72+
* Override this method to provide before-model logic.
73+
*
74+
* @param mixed $payload The input to process
75+
* @param RuntimeConfig $config The runtime context
76+
* @param Closure $next The next stage in the pipeline
77+
*
78+
* @return mixed The processed result
79+
*/
80+
public function beforeModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
81+
{
82+
return $this->handlePipeable($payload, $config, $next);
83+
}
84+
85+
/**
86+
* Hook that runs after the model call.
87+
* Default implementation delegates to handlePipeable().
88+
* Override this method to provide after-model logic.
89+
*
90+
* @param mixed $payload The input to process
91+
* @param RuntimeConfig $config The runtime context
92+
* @param Closure $next The next stage in the pipeline
93+
*
94+
* @return mixed The processed result
95+
*/
96+
public function afterModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
97+
{
98+
return $this->handlePipeable($payload, $config, $next);
99+
}
100+
101+
/**
102+
* Handle the pipeline processing.
103+
* Default implementation passes through unchanged.
104+
* Override this method to provide default logic used by all hooks,
105+
* or override individual hook methods for hook-specific logic.
106+
*
107+
* @param mixed $payload The input to process
108+
* @param RuntimeConfig $config The runtime context
109+
* @param Closure $next The next stage in the pipeline
110+
*
111+
* @return mixed The processed result
112+
*/
113+
public function handlePipeable(mixed $payload, RuntimeConfig $config, Closure $next): mixed
114+
{
115+
return $next($payload, $config);
116+
}
117+
}
118+

src/Agents/Middleware/AfterModelClosureMiddleware.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ public function handlePipeable(mixed $payload, RuntimeConfig $config, Closure $n
2525
{
2626
return ($this->closure)($payload, $config, $next);
2727
}
28+
29+
public function afterModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
30+
{
31+
return $this->handlePipeable($payload, $config, $next);
32+
}
2833
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\Agents\Middleware;
6+
7+
use Closure;
8+
use Cortex\Pipeline\RuntimeConfig;
9+
use Cortex\Support\Traits\CanPipe;
10+
use Cortex\Agents\Contracts\AfterModelMiddleware;
11+
use Cortex\Agents\Contracts\Middleware;
12+
13+
/**
14+
* Wrapper that delegates to afterModel() if it exists, otherwise handlePipeable().
15+
* Used when middleware implements both BeforeModelMiddleware and AfterModelMiddleware.
16+
*/
17+
class AfterModelWrapper implements AfterModelMiddleware
18+
{
19+
use CanPipe;
20+
21+
public function __construct(
22+
protected Middleware $middleware,
23+
) {}
24+
25+
public function handlePipeable(mixed $payload, RuntimeConfig $config, Closure $next): mixed
26+
{
27+
/** @var AfterModelMiddleware $middleware */
28+
$middleware = $this->middleware;
29+
30+
return $middleware->afterModel($payload, $config, $next);
31+
}
32+
33+
public function afterModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
34+
{
35+
return $this->handlePipeable($payload, $config, $next);
36+
}
37+
}
38+

src/Agents/Middleware/BeforeModelClosureMiddleware.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ public function handlePipeable(mixed $payload, RuntimeConfig $config, Closure $n
2525
{
2626
return ($this->closure)($payload, $config, $next);
2727
}
28+
29+
public function beforeModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
30+
{
31+
return $this->handlePipeable($payload, $config, $next);
32+
}
2833
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cortex\Agents\Middleware;
6+
7+
use Closure;
8+
use Cortex\Pipeline\RuntimeConfig;
9+
use Cortex\Support\Traits\CanPipe;
10+
use Cortex\Agents\Contracts\BeforeModelMiddleware;
11+
use Cortex\Agents\Contracts\Middleware;
12+
13+
/**
14+
* Wrapper that delegates to beforeModel() if it exists, otherwise handlePipeable().
15+
* Used when middleware implements both BeforeModelMiddleware and AfterModelMiddleware.
16+
*/
17+
class BeforeModelWrapper implements BeforeModelMiddleware
18+
{
19+
use CanPipe;
20+
21+
public function __construct(
22+
protected Middleware $middleware,
23+
) {}
24+
25+
public function handlePipeable(mixed $payload, RuntimeConfig $config, Closure $next): mixed
26+
{
27+
/** @var BeforeModelMiddleware $middleware */
28+
$middleware = $this->middleware;
29+
30+
return $middleware->beforeModel($payload, $config, $next);
31+
}
32+
33+
public function beforeModel(mixed $payload, RuntimeConfig $config, Closure $next): mixed
34+
{
35+
return $this->handlePipeable($payload, $config, $next);
36+
}
37+
}
38+

src/Agents/Middleware/BeforePromptClosureMiddleware.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,9 @@ public function handlePipeable(mixed $payload, RuntimeConfig $config, Closure $n
2525
{
2626
return ($this->closure)($payload, $config, $next);
2727
}
28+
29+
public function beforePrompt(mixed $payload, RuntimeConfig $config, Closure $next): mixed
30+
{
31+
return $this->handlePipeable($payload, $config, $next);
32+
}
2833
}

0 commit comments

Comments
 (0)