55namespace App \Services \AI ;
66
77
8- use App \Models \Ai \Tools \AiTool ;
98use App \Services \AI \Exception \ModelIdNotAvailableException ;
109use App \Services \AI \Exception \ModelNotInPayloadException ;
1110use App \Services \AI \Exception \NoModelSetInRequestException ;
12- use App \Services \AI \Tools \ToolExecutionService ;
1311use App \Services \AI \Value \AiModel ;
1412use App \Services \AI \Value \AiRequest ;
1513use App \Services \AI \Value \AiResponse ;
1614use App \Services \AI \Value \AvailableAiModels ;
1715use App \Services \AI \Value \ModelUsageType ;
1816use Illuminate \Container \Attributes \Singleton ;
19- use Illuminate \Support \Facades \Log ;
2017
2118#[Singleton]
2219readonly class AiService
2320{
2421 public function __construct (
25- private AiFactory $ factory ,
26- private ToolExecutionService $ toolExecutionService
22+ private AiFactory $ factory
2723 )
2824 {
2925 }
@@ -77,48 +73,12 @@ public function getModelOrFail(string $modelId, ?bool $external = null): AiModel
7773 * If the response contains tool calls, they will be automatically executed and a follow-up request will be sent.
7874 *
7975 * @param array|AiRequest $request Either an AiRequest object or an array representing the request payload.
80- * @param int $maxToolRounds Maximum number of tool execution rounds to prevent infinite loops
8176 * @return AiResponse
8277 */
83- public function sendRequest (array |AiRequest $ request, int $ maxToolRounds = 2 ): AiResponse
78+ public function sendRequest (array |AiRequest $ request ): AiResponse
8479 {
8580 [$ request , $ model ] = $ this ->resolveRequestAndModel ($ request );
86-
87- $ round = 0 ;
88- $ currentRequest = $ request ;
89- while (true ) {
90- $ response = $ model ->getClient ()->sendRequest ($ currentRequest );
91- // \Log::info($response->content);
92- if (!$ this ->toolExecutionService ->requiresToolExecution ($ response )) {
93- return $ response ;
94- }
95-
96- Log::info ('AiTool execution required ' , [
97- 'round ' => $ round + 1 ,
98- 'tool_count ' => count ($ response ->toolCalls ),
99- ]);
100-
101- $ round ++;
102-
103- // If we've reached max rounds, send final request without tools
104- if ($ round >= $ maxToolRounds ) {
105- Log::warning ('Max tool execution rounds reached ' , ['max_rounds ' => $ maxToolRounds ]);
106-
107- $ currentRequest = $ this ->toolExecutionService ->buildFollowUpRequest (
108- $ currentRequest ,
109- $ response ,
110- disableTools: true
111- );
112-
113- return $ model ->getClient ()->sendRequest ($ currentRequest );
114- }
115-
116- // Build follow-up request with tool results
117- $ currentRequest = $ this ->toolExecutionService ->buildFollowUpRequest (
118- $ currentRequest ,
119- $ response
120- );
121- }
81+ return $ model ->getClient ()->sendRequest ($ request );
12282 }
12383
12484 /**
@@ -129,108 +89,12 @@ public function sendRequest(array|AiRequest $request, int $maxToolRounds = 2): A
12989 * @param array|AiRequest $request Either an AiRequest object or an array representing the request payload.
13090 * @param callable(AiResponse $response): void $onData A callback function that will be called with each chunk of data received.
13191 * The function should accept a single parameter of type AiResponse.
132- * @param int $maxToolRounds Maximum number of tool execution rounds to prevent infinite loops
13392 * @return void
13493 */
135- public function sendStreamRequest (array |AiRequest $ request , callable $ onData, int $ maxToolRounds = 5 ): void
94+ public function sendStreamRequest (array |AiRequest $ request , callable $ onData ): void
13695 {
13796 [$ request , $ model ] = $ this ->resolveRequestAndModel ($ request );
138-
139- $ round = 0 ;
140- $ currentRequest = $ request ;
141- $ lastCompleteResponse = null ;
142-
143- while (true ) {
144- // Wrap onData to capture the final complete response and mask tool call completion
145- $ wrappedOnData = function (AiResponse $ response ) use ($ onData , &$ lastCompleteResponse , $ round ) {
146- // If this is a tool call completion, don't tell the frontend it's done yet
147- // We'll continue with follow-up requests
148- if ($ response ->isDone && $ response ->finishReason === 'tool_calls ' ) {
149- // Create a modified response with isDone=false for the frontend
150- $ frontendResponse = new AiResponse (
151- content: $ response ->content ,
152- usage: $ response ->usage ,
153- isDone: false , // Mask the completion
154- error: $ response ->error ,
155- toolCalls: $ response ->toolCalls ,
156- finishReason: $ response ->finishReason ,
157- type: $ response ->type ,
158- status: $ response ->status
159- );
160- $ onData ($ frontendResponse );
161- $ lastCompleteResponse = $ response ; // Keep the real response internally
162- } else {
163- // Normal response or final completion - send as is
164- $ onData ($ response );
165- if ($ response ->isDone ) {
166- $ lastCompleteResponse = $ response ;
167- }
168- }
169- };
170-
171- // Send the streaming request
172- $ model ->getClient ()->sendStreamRequest ($ currentRequest , $ wrappedOnData );
173-
174- // Check if tool execution is needed
175- if (!$ lastCompleteResponse || !$ this ->toolExecutionService ->requiresToolExecution ($ lastCompleteResponse )) {
176- // No tools needed or response complete, we're done
177- return ;
178- }
179- $ round ++;
180-
181- // Check if we've reached max rounds
182- if ($ round >= $ maxToolRounds ) {
183- Log::warning ('Max tool execution rounds reached in stream ' , ['max_rounds ' => $ maxToolRounds ]);
184-
185- // Send status about max rounds
186- $ onData (
187- new AiResponse (
188- content: ['text ' => '' ],
189- isDone: false ,
190- type: 'status ' ,
191- status: [
192- 'key ' => 'max_execution ' ,
193- 'value ' => 'Maximum tool execution rounds reached. Generating final response... '
194- ]
195- ));
196-
197- // Build final request with tools disabled
198- $ currentRequest = $ this ->toolExecutionService ->buildFollowUpRequest (
199- $ currentRequest ,
200- $ lastCompleteResponse ,
201- disableTools: true
202- );
203- // Send the final request directly and return
204- $ model ->getClient ()->sendStreamRequest ($ currentRequest , $ onData );
205- return ;
206- }
207-
208- // Send status message about tool execution
209- $ toolNames = array_map (fn ($ tc ) => $ tc ->name , $ lastCompleteResponse ->toolCalls );
210- $ capabilities = [];
211- foreach ($ toolNames as $ toolName ) {
212- $ tool = AiTool::where ('name ' , $ toolName )->firstOrFail ();
213- $ capabilities [] = $ tool ->capability ;
214- }
215- // \Log::info($capabilities);
216- $ onData (new AiResponse (
217- content: ['text ' => '' ],
218- isDone: false ,
219- type: 'status ' ,
220- status: [
221- 'key ' => 'tool_call ' ,
222- 'value ' => $ capabilities
223- ]
224- ));
225- // Build follow-up request with tool results
226- $ currentRequest = $ this ->toolExecutionService ->buildFollowUpRequest (
227- $ currentRequest ,
228- $ lastCompleteResponse
229- );
230-
231- // Note: Don't reset $lastCompleteResponse to null here
232- // It will be overwritten in the next iteration when isDone=true
233- }
97+ $ model ->getClient ()->sendStreamRequest ($ request , $ onData );
23498 }
23599
236100 /**
0 commit comments