@@ -142,6 +142,8 @@ protected function buildPrompt(array $contextData, string $trigger, bool $isPro)
142142
143143 $ system = "Você é o Vertex Bot - um mentor financeiro amigável e acolhedor. Dê uma dica personalizada em português do Brasil. " ;
144144 $ system .= "NUNCA use nome, CPF ou dados pessoais. Responda apenas com o texto da dica, sem introduções. \n\n" ;
145+ $ system .= "VARIE sempre o tema: não repita a mesma ideia (ex.: evitar ficar só em 'reserva de emergência' e 'rotativo'). " ;
146+ $ system .= "Alternar entre: controle de gastos, orçamento por categoria, metas, lazer sem culpa, assinaturas, parcelas, regra 50/30/20, pequenas economias, comparação de preços, etc. \n\n" ;
145147
146148 if ($ isLowScore ) {
147149 $ system .= "IMPORTANTE - usuário com score baixo (provavelmente leigo em finanças): " ;
@@ -176,4 +178,180 @@ protected function buildPrompt(array $contextData, string $trigger, bool $isPro)
176178
177179 return $ system . $ context . "\nDica: " ;
178180 }
181+
182+ /**
183+ * Generate a 3-paragraph strategic conclusion for the consulting PDF.
184+ * Formal, technical style. Cites "Regra 50/30/20 da Vertex". Returns null on failure.
185+ *
186+ * @param array{budget_analysis: array, financial_score: int, metrics?: array} $contextData
187+ */
188+ public function generateConsultingConclusion (array $ contextData ): ?string
189+ {
190+ $ apiKey = $ this ->getApiKey ();
191+ if (! $ apiKey ) {
192+ return null ;
193+ }
194+
195+ $ prompt = $ this ->buildConsultingConclusionPrompt ($ contextData );
196+
197+ try {
198+ $ url = self ::API_BASE . '/models/ ' . self ::MODEL . ':generateContent?key= ' . $ apiKey ;
199+ $ response = $ this ->http ->post ($ url , [
200+ RequestOptions::JSON => [
201+ 'contents ' => [
202+ [
203+ 'parts ' => [
204+ ['text ' => $ prompt ],
205+ ],
206+ ],
207+ ],
208+ 'generationConfig ' => [
209+ 'maxOutputTokens ' => 512 ,
210+ 'temperature ' => 0.6 ,
211+ 'topP ' => 0.9 ,
212+ ],
213+ ],
214+ ]);
215+
216+ $ body = json_decode ((string ) $ response ->getBody (), true );
217+ $ text = $ body ['candidates ' ][0 ]['content ' ]['parts ' ][0 ]['text ' ] ?? null ;
218+
219+ if ($ text === null || trim ($ text ) === '' ) {
220+ return null ;
221+ }
222+
223+ return trim ($ text );
224+ } catch (GuzzleException $ e ) {
225+ Log::warning ('Gemini API consulting conclusion failed ' , ['message ' => $ e ->getMessage ()]);
226+
227+ return null ;
228+ } catch (\Throwable $ e ) {
229+ Log::warning ('Gemini API consulting conclusion error ' , ['message ' => $ e ->getMessage ()]);
230+
231+ return null ;
232+ }
233+ }
234+
235+ /**
236+ * Build prompt for consulting conclusion. Token-optimized, formal.
237+ *
238+ * @param array{budget_analysis: array, financial_score: int, metrics?: array} $contextData
239+ */
240+ protected function buildConsultingConclusionPrompt (array $ contextData ): string
241+ {
242+ $ score = $ contextData ['financial_score ' ] ?? 0 ;
243+ $ budget = $ contextData ['budget_analysis ' ] ?? [];
244+ $ pillars = $ budget ['pillars ' ] ?? [];
245+ $ metrics = $ contextData ['metrics ' ] ?? [];
246+ $ savingsPct = $ budget ['savings_pct ' ] ?? 0 ;
247+ $ income = $ metrics ['income ' ] ?? 0 ;
248+ $ expense = $ metrics ['expense ' ] ?? 0 ;
249+ $ balance = $ metrics ['account_balance ' ] ?? 0 ;
250+
251+ $ essential = $ pillars ['essential ' ] ?? [];
252+ $ lifestyle = $ pillars ['lifestyle ' ] ?? [];
253+ $ financial = $ pillars ['financial ' ] ?? [];
254+
255+ $ deviations = [];
256+ if (($ essential ['status ' ] ?? '' ) === 'over ' ) {
257+ $ deviations [] = 'Essencial acima da meta ( ' . ($ essential ['deviation ' ] ?? 0 ) . '%) ' ;
258+ }
259+ if (($ essential ['status ' ] ?? '' ) === 'under ' ) {
260+ $ deviations [] = 'Essencial abaixo da meta ' ;
261+ }
262+ if (($ lifestyle ['status ' ] ?? '' ) === 'over ' ) {
263+ $ deviations [] = 'Estilo de vida acima da meta ( ' . ($ lifestyle ['deviation ' ] ?? 0 ) . '%) ' ;
264+ }
265+ if (($ financial ['status ' ] ?? '' ) === 'under ' ) {
266+ $ deviations [] = 'Financeiro abaixo da meta ' ;
267+ }
268+ if ($ savingsPct < 20 ) {
269+ $ deviations [] = 'Taxa de poupança ' . round ($ savingsPct , 1 ) . '% (meta 20%) ' ;
270+ }
271+
272+ $ devText = empty ($ deviations ) ? 'Aderência adequada à regra ' : implode ('; ' , $ deviations );
273+
274+ $ system = "Você é o CFO da Vertex. Escreva a Conclusão Estratégica do Especialista para um relatório financeiro. " ;
275+ $ system .= "3 parágrafos, formal e técnico. Use metodologia Regra 50/30/20 da Vertex. " ;
276+ $ system .= "Cite oportunidades baseadas nos desvios encontrados. Sem nome ou dados pessoais. Responda apenas o texto. \n\n" ;
277+
278+ $ context = "Contexto: \n" ;
279+ $ context .= "- Score: {$ score }/100 | Poupança real: " . round ($ savingsPct , 1 ) . "% \n" ;
280+ $ context .= "- Desvios: {$ devText }\n" ;
281+ $ context .= "- Renda: R \$ " . number_format ($ income , 2 , ', ' , '. ' ) . " | Despesas: R \$ " . number_format ($ expense , 2 , ', ' , '. ' ) . " | Saldo: R \$ " . number_format ($ balance , 2 , ', ' , '. ' ) . "\n" ;
282+
283+ return $ system . $ context . "\nConclusão estratégica: " ;
284+ }
285+
286+ /**
287+ * Generate 1-year patrimony projection based on current savings rate.
288+ *
289+ * @param array{reserve_months: float, savings_rate: float, balance: float, monthly_income: float, monthly_expense: float} $contextData
290+ */
291+ public function generateOneYearProjection (array $ contextData ): ?string
292+ {
293+ $ apiKey = $ this ->getApiKey ();
294+ if (! $ apiKey ) {
295+ return null ;
296+ }
297+
298+ $ prompt = $ this ->buildProjectionPrompt ($ contextData );
299+
300+ try {
301+ $ url = self ::API_BASE . '/models/ ' . self ::MODEL . ':generateContent?key= ' . $ apiKey ;
302+ $ response = $ this ->http ->post ($ url , [
303+ RequestOptions::JSON => [
304+ 'contents ' => [
305+ [
306+ 'parts ' => [
307+ ['text ' => $ prompt ],
308+ ],
309+ ],
310+ ],
311+ 'generationConfig ' => [
312+ 'maxOutputTokens ' => 256 ,
313+ 'temperature ' => 0.6 ,
314+ 'topP ' => 0.9 ,
315+ ],
316+ ],
317+ ]);
318+
319+ $ body = json_decode ((string ) $ response ->getBody (), true );
320+ $ text = $ body ['candidates ' ][0 ]['content ' ]['parts ' ][0 ]['text ' ] ?? null ;
321+
322+ if ($ text === null || trim ($ text ) === '' ) {
323+ return null ;
324+ }
325+
326+ return trim ($ text );
327+ } catch (GuzzleException $ e ) {
328+ Log::warning ('Gemini API projection failed ' , ['message ' => $ e ->getMessage ()]);
329+
330+ return null ;
331+ } catch (\Throwable $ e ) {
332+ Log::warning ('Gemini API projection error ' , ['message ' => $ e ->getMessage ()]);
333+
334+ return null ;
335+ }
336+ }
337+
338+ /**
339+ * @param array{reserve_months: float, savings_rate: float, balance: float, monthly_income: float, monthly_expense: float} $contextData
340+ */
341+ protected function buildProjectionPrompt (array $ contextData ): string
342+ {
343+ $ reserveMonths = $ contextData ['reserve_months ' ] ?? 0 ;
344+ $ savingsRate = $ contextData ['savings_rate ' ] ?? 0 ;
345+ $ balance = $ contextData ['balance ' ] ?? 0 ;
346+ $ income = $ contextData ['monthly_income ' ] ?? 0 ;
347+ $ expense = $ contextData ['monthly_expense ' ] ?? 0 ;
348+
349+ $ system = "Você é o Vertex Bot CFO. Projete o patrimônio do usuário em 1 ano com base nos dados atuais. " ;
350+ $ system .= "2 a 4 frases, português Brasil. Cite a Regra 50/30/20 da Vertex. Sem nome ou PII. Apenas o texto. \n\n" ;
351+
352+ $ context = "Saldo: R \$ " . number_format ($ balance , 2 , ', ' , '. ' ) . " | Reserva: " . round ($ reserveMonths , 1 ) . " meses | " ;
353+ $ context .= "Taxa poupança: " . round ($ savingsRate , 1 ) . "% | Renda: R \$ " . number_format ($ income , 2 , ', ' , '. ' ) . " | Despesas: R \$ " . number_format ($ expense , 2 , ', ' , '. ' ) . "\n" ;
354+
355+ return $ system . $ context . "\nProjeção em 1 ano: " ;
356+ }
179357}
0 commit comments