diff --git a/src/Purrse.Api/Services/ChatService.cs b/src/Purrse.Api/Services/ChatService.cs index 9675c6e..0b224c1 100644 --- a/src/Purrse.Api/Services/ChatService.cs +++ b/src/Purrse.Api/Services/ChatService.cs @@ -227,8 +227,28 @@ public class ChatService : IChatService } else { + var rawContent = response?.Message?.Content ?? "I'm sorry, I couldn't process that request."; + + // If the model responded without calling any tools on the first attempt, + // it likely fabricated data or generated code. Nudge it to use tools. + if (toolResults.Count == 0 && i == 0) + { + _logger.LogWarning("Model responded without calling tools on first attempt, retrying with nudge"); + ollamaMessages.Add(new OllamaToolMessage + { + Role = "assistant", + Content = rawContent + }); + ollamaMessages.Add(new OllamaToolMessage + { + Role = "user", + Content = "You did not use any tools. You MUST call the provided function tools to look up or modify my financial data. Do not make up data. Try again." + }); + continue; + } + // No tool calls - save final assistant message - var content = StripCodeBlocks(response?.Message?.Content ?? "I'm sorry, I couldn't process that request."); + var content = StripCodeBlocks(rawContent); assistantDbMessage = new ChatMessage { Id = Guid.NewGuid(), @@ -417,13 +437,22 @@ public class ChatService : IChatService var json = JsonSerializer.Serialize(request, OllamaJsonOptions); var content = new StringContent(json, Encoding.UTF8, "application/json"); - _logger.LogDebug("Sending chat request to Ollama: {Url}", $"{ollamaUrl}/api/chat"); + _logger.LogInformation("Ollama request — model: {Model}, tools: {ToolCount}, messages: {MessageCount}", + request.Model, request.Tools?.Count ?? 0, request.Messages.Count); var response = await client.PostAsync($"{ollamaUrl}/api/chat", content); response.EnsureSuccessStatusCode(); var responseBody = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(responseBody, OllamaJsonOptions); + _logger.LogInformation("Ollama response — length: {Length}, preview: {Preview}", + responseBody.Length, responseBody.Length > 500 ? responseBody[..500] : responseBody); + + var parsed = JsonSerializer.Deserialize(responseBody, OllamaJsonOptions); + _logger.LogInformation("Ollama parsed — tool_calls: {ToolCallCount}, content_length: {ContentLength}", + parsed?.Message?.ToolCalls?.Count ?? 0, + parsed?.Message?.Content?.Length ?? 0); + + return parsed; } private async Task<(string toolResultType, object data, string summaryForLlm)> ExecuteToolAsync(Guid userId, OllamaToolCall toolCall) @@ -1108,6 +1137,16 @@ public class ChatService : IChatService }; } + // Detect responses that contain code blocks or common code patterns + private static bool LooksLikeCodeResponse(string content) + { + if (content.Contains("```")) return true; + if (content.Contains("import ") && content.Contains("(")) return true; + if (content.Contains("def ") && content.Contains("():")) return true; + if (content.Contains("print(") || content.Contains("console.log(")) return true; + return false; + } + // Strip fenced code blocks that smaller models sometimes generate instead of using tools private static readonly Regex CodeBlockRegex = new(@"```[\s\S]*?```", RegexOptions.Compiled); private static string StripCodeBlocks(string content)