Skip to content

Commit 2d2c2db

Browse files
committed
AI flows code review
1 parent 1d9c14f commit 2d2c2db

File tree

15 files changed

+370
-255
lines changed

15 files changed

+370
-255
lines changed

app/base/abstracts/Models/AbstractLLMAdapter.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,21 @@ public function sendRaw(array $payload) : array
3030

3131
$prepared = $this->prepareRequest($payload);
3232

33+
if (App::getInstance()->getEnvironment()->canDebug()) {
34+
$this->getApplicationLogger()->debug("Sending LLM request <pre>" . json_encode(['endpoint' => $this->getEndpoint(), 'payload' => $prepared], JSON_PRETTY_PRINT) . "</pre>");
35+
}
36+
3337
$resp = $client->post(
3438
$this->getEndpoint(),
3539
$prepared
3640
);
3741

3842
$responseBody = $resp->getBody()->getContents();
3943

44+
if (App::getInstance()->getEnvironment()->canDebug()) {
45+
$this->getApplicationLogger()->debug("Received LLM response <pre>" . json_encode(['response' => $responseBody], JSON_PRETTY_PRINT) . "</pre>");
46+
}
47+
4048
if (!isJson($responseBody)) {
4149
throw new Exception("Invalid response from LLM API");
4250
}

app/base/ai/Actions/GraphQLSchemaProvider.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace App\Base\AI\Actions;
1515

16+
use App\App;
1617
use GraphQL\Utils\BuildClientSchema;
1718
use GraphQL\Utils\SchemaPrinter;
1819
use GraphQL\Type\Schema;
@@ -28,6 +29,9 @@
2829

2930
class GraphQLSchemaProvider
3031
{
32+
33+
const INTROSPECTION_CACHE_KEY = 'graphql.introspection.full_schema';
34+
3135
const INTROSPECTION_QUERY = '
3236
query IntrospectionQuery {
3337
__schema {
@@ -103,7 +107,12 @@ public function __construct(GraphQLExecutor $executor)
103107

104108
public function getFullSchema(): string
105109
{
106-
$result = $this->executor->execute(self::INTROSPECTION_QUERY);
110+
if (App::getInstance()->getCache()->has(self::INTROSPECTION_CACHE_KEY)) {
111+
$result = App::getInstance()->getCache()->get(self::INTROSPECTION_CACHE_KEY);
112+
} else {
113+
$result = $this->executor->execute(self::INTROSPECTION_QUERY);
114+
App::getInstance()->getCache()->set(self::INTROSPECTION_CACHE_KEY, $result);
115+
}
107116

108117
if (!isset($result['data']['__schema'])) {
109118
return '';
@@ -120,7 +129,12 @@ public function getFullSchema(): string
120129
*/
121130
public function getSchemaFilteredByTypes(array $rootTypes): string
122131
{
123-
$result = $this->executor->execute(self::INTROSPECTION_QUERY);
132+
if (App::getInstance()->getCache()->has(self::INTROSPECTION_CACHE_KEY)) {
133+
$result = App::getInstance()->getCache()->get(self::INTROSPECTION_CACHE_KEY);
134+
} else {
135+
$result = $this->executor->execute(self::INTROSPECTION_QUERY);
136+
App::getInstance()->getCache()->set(self::INTROSPECTION_CACHE_KEY, $result);
137+
}
124138

125139
if (!isset($result['data']['__schema'])) {
126140
return '';

app/base/ai/Actions/Orchestrator.php

Lines changed: 11 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace App\Base\AI\Actions;
1515

1616
use App\Base\AI\Flows\BaseFlow;
17-
use App\Base\AI\Models\GoogleGemini;
1817
use App\Base\Interfaces\AI\AIModelInterface;
1918
use Exception;
2019

@@ -29,70 +28,33 @@ public function __construct(AIModelInterface $llm)
2928
}
3029

3130
/**
32-
* Registra un tool invocabile dal modello
31+
* Register a tool handler
3332
*/
3433
public function registerTool(string $name, callable $handler) : void
3534
{
3635
$this->toolsRegistry[$name] = $handler;
3736
}
3837

3938
/**
40-
* Avvia il flow completo.
39+
* Run a flow
4140
*/
4241
public function runFlow(BaseFlow $flow, string $userPrompt) : array
4342
{
44-
//
45-
// 1) Costruisco i messaggi iniziali
46-
//
47-
$messages = $this->llm->buildFlowInitialMessages($flow, $userPrompt);
48-
49-
50-
//
51-
// 2) Prima chiamata al modello
52-
//
53-
if ($this->llm instanceof GoogleGemini) {
54-
55-
// Costruiamo le dichiarazioni dei tool
56-
$functions = [];
57-
foreach ($flow->tools() as $name => $schema) {
58-
$functions[] = [
59-
'name' => $name,
60-
'description' => $schema['description'] ?? '',
61-
'parameters' => $schema['parameters'] ?? ['type' => 'object']
62-
];
63-
}
43+
$payload = $this->llm->buildFlowInitialRequest($flow, $userPrompt);
6444

65-
$response = $this->llm->sendRaw([
66-
'contents' => $messages,
67-
'tools' => [
68-
[
69-
'function_declarations' => $functions
70-
]
71-
]
72-
]);
73-
74-
} else {
75-
// fallback per altri LLM (OpenAI, Anthropic, ecc.)
76-
$response = $this->llm->sendRaw([
77-
'messages' => $messages
78-
]);
79-
}
45+
// extract messages for history tracking
46+
$messages = $payload['messages'] ?? $payload['contents'] ?? [];
8047

48+
$response = $this->llm->sendRaw($payload);
8149

82-
//
83-
// 3) Normalizziamo la risposta
84-
//
8550
$normalized = $this->llm->normalizeResponse($response);
8651

87-
88-
//
89-
// 4) Ciclo finché il modello continua a chiamare funzioni
90-
//
9152
while (!empty($normalized['functionCalls'])) {
9253

9354
$call = $normalized['functionCalls'][0];
9455
$functionName = $call['name'] ?? null;
9556
$args = $call['args'] ?? [];
57+
$id = $call['id'] ?? null;
9658

9759
if (!$functionName) {
9860
throw new Exception("FunctionCall senza 'name'");
@@ -102,31 +64,16 @@ public function runFlow(BaseFlow $flow, string $userPrompt) : array
10264
throw new Exception("Tool non registrato: {$functionName}");
10365
}
10466

105-
//
106-
// 4a) Gemini richiede che il function_call originale
107-
// sia inserito nella history ESATTAMENTE com'era.
108-
//
10967
if (!empty($normalized['rawFunctionMessages'][0])) {
11068
$messages[] = $normalized['rawFunctionMessages'][0];
11169
} else {
112-
// fallback sicuro per altri LLM
113-
$messages[] = [
114-
'role' => 'model',
115-
'parts' => [
116-
[
117-
'function_call' => [
118-
'name' => $functionName,
119-
'args' => $args
120-
]
121-
]
122-
]
123-
];
70+
$assistantMessage = $this->llm->formatAssistantFunctionCallMessage($functionName, $args);
71+
if (!is_null($assistantMessage)) {
72+
$messages[] = $assistantMessage;
73+
}
12474
}
12575

12676

127-
//
128-
// 4b) Eseguiamo il tool
129-
//
13077
$handler = $this->toolsRegistry[$functionName];
13178

13279
if (is_string($args)) {
@@ -137,32 +84,16 @@ public function runFlow(BaseFlow $flow, string $userPrompt) : array
13784
$toolResult = $handler($args);
13885

13986

140-
//
141-
// 4c) Rispondiamo al modello con function_response
142-
// e con TUTTA la history (compreso il function_call originale)
143-
//
14487
$response = $this->llm->sendFunctionResponse(
14588
$functionName,
14689
$toolResult,
14790
$messages
14891
);
14992

150-
//
151-
// 4d) Normalizziamo la nuova risposta
152-
//
15393
$normalized = $this->llm->normalizeResponse($response);
154-
155-
//
156-
// ⚠️ IMPORTANTE:
157-
// NON aggiungere assistantText alla history qui.
158-
// I messaggi testuali vanno aggiunti solo quando la chain finisce.
159-
//
16094
}
16195

16296

163-
//
164-
// 5) Fine del flow: ritorna il risultato finale
165-
//
16697
return $normalized;
16798
}
16899
}

app/base/ai/Flows/BaseFlow.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ abstract class BaseFlow
1717
{
1818
abstract public function systemPrompt() : string;
1919

20-
/**
21-
* Tools dichiarati dal flow (schema per il modello)
22-
*/
2320
abstract public function tools() : array;
2421

2522
public function schema(): string

app/base/ai/Flows/EcommerceFlow.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,40 @@
1313

1414
namespace App\Base\AI\Flows;
1515

16+
use App\App;
1617
use App\Base\AI\Actions\GraphQLSchemaProvider;
18+
use HaydenPierce\ClassFinder\ClassFinder;
19+
use App\Base\Interfaces\Model\ProductInterface;
1720

1821
class EcommerceFlow extends BaseFlow
1922
{
2023
protected string $schema;
2124

2225
public function __construct(GraphQLSchemaProvider $schemaProvider)
2326
{
24-
$this->schema = $schemaProvider->getSchemaFilteredByTypes([
27+
// Get all models related to ecommerce
28+
// Base Types
29+
$types = [
2530
'Cart',
2631
'CartItem',
2732
'CartDiscount',
2833
'ProductInterface',
2934
'PhysicalProductInterface',
30-
'GiftCard',
31-
'Book',
32-
'DownloadableProduct',
3335
'ProductStock',
34-
]);
36+
];
37+
38+
// Product Types
39+
$types = array_merge($types, array_map(function($el) {
40+
return App::getInstance()->getClassBasename($el);
41+
}, array_filter(
42+
array_merge(
43+
ClassFinder::getClassesInNamespace(App::MODELS_NAMESPACE, ClassFinder::RECURSIVE_MODE),
44+
ClassFinder::getClassesInNamespace(App::BASE_MODELS_NAMESPACE, ClassFinder::RECURSIVE_MODE)
45+
),
46+
fn ($className) => is_subclass_of($className, ProductInterface::class)
47+
)));
48+
49+
$this->schema = $schemaProvider->getSchemaFilteredByTypes($types);
3550
}
3651

3752

app/base/ai/Models/ChatGPT.php

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,31 @@ public function formatUserMessage(string $prompt): array
7575
];
7676
}
7777

78+
public function formatAssistantMessage(mixed $message, ?string $messageType = null): array
79+
{
80+
return [
81+
'role' => 'assistant',
82+
$messageType ?? 'content' => $message
83+
];
84+
}
85+
86+
public function formatAssistantFunctionCallMessage(string $functionName, array $args, ?string $id = null): array
87+
{
88+
return [
89+
'role' => 'assistant',
90+
'tool_calls' => [
91+
[
92+
'id' => $id ?? $functionName,
93+
'type' => 'function',
94+
'function' => [
95+
'name' => $functionName,
96+
'arguments' => json_encode($args, JSON_UNESCAPED_UNICODE),
97+
],
98+
],
99+
],
100+
];
101+
}
102+
78103
public function buildConversation(array $previousMessages, string $prompt, ?string $model = null): array
79104
{
80105
$messages = $previousMessages;
@@ -125,14 +150,12 @@ public function normalizeResponse(array $raw): array
125150
];
126151
}
127152

128-
public function buildFlowInitialMessages(BaseFlow $flow, string $userPrompt): array
153+
public function buildFlowInitialRequest(BaseFlow $flow, string $userPrompt, ?string $model = null): array
129154
{
130155
$messages = [
131156
[
132157
'role' => 'system',
133-
'parts' => [
134-
['text' => $flow->systemPrompt()]
135-
]
158+
'content' => $flow->systemPrompt()
136159
],
137160
];
138161

@@ -143,30 +166,34 @@ public function buildFlowInitialMessages(BaseFlow $flow, string $userPrompt): ar
143166
];
144167
}
145168

146-
$messages[] = [
147-
'role' => 'user',
148-
'parts' => [
149-
['text' => $userPrompt]
150-
]
151-
];
169+
$messages[] = $this->formatUserMessage($userPrompt);
152170

153-
$messages[] = [
154-
'role' => 'model',
155-
'parts' => [
156-
['text' => json_encode($flow->tools())]
157-
]
158-
];
171+
$tools = [];
172+
if (!empty($flow->tools())) {
173+
foreach ($flow->tools() as $toolName => $tool) {
174+
$tools[] = [
175+
'name' => $toolName,
176+
'description' => $tool['description'] ?? '',
177+
'parameters' => $tool['parameters'] ?? [],
178+
];
179+
}
180+
}
159181

160-
return array_values($messages);
182+
return [
183+
'model' => $this->getDefaultModel(),
184+
'tools' => $tools,
185+
'messages' => array_values($messages),
186+
];
161187
}
162188

163-
public function sendFunctionResponse(string $name, array $result, array &$history = []): array
189+
public function sendFunctionResponse(string $name, array $result, array &$history = [], ?string $id = null): array
164190
{
165191
return $this->sendRaw([
192+
'model' => $this->getDefaultModel(),
166193
'messages' => [
167194
[
168195
'role' => 'tool',
169-
'tool_call_id' => $name,
196+
'tool_call_id' => $id ?? $name,
170197
'content' => json_encode($result)
171198
]
172199
]

0 commit comments

Comments
 (0)