Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions flo_ai/flo_ai/llm/gemini_llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,9 @@ def get_next_chunk():
def get_message_content(self, response: Dict[str, Any]) -> str:
if isinstance(response, str):
return response
if hasattr(response, 'content') and response.content is not None:
return str(response.content)
return ''
if isinstance(response, dict):
content = response.get('content')
return str(content) if content is not None else ''
Comment on lines 208 to +213
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: missing fallback return — non-str/non-dict inputs now implicitly return None, breaking callers and existing tests.

Previously, get_message_content ended with return '' (or str(response.content) via hasattr), so non-str/non-dict inputs always produced a string. After this change, any input that is neither str nor dict falls through and returns None implicitly, which:

  1. Breaks flo_ai/flo_ai/agent/agent.py:616, which chains .strip().upper() on the result (self.llm.get_message_content(analysis_response).strip().upper()). A None return raises AttributeError: 'NoneType' object has no attribute 'strip'.
  2. Breaks the existing integration test test_get_message_content_object in flo_ai/tests/integration-tests/test_gemini_llm_real.py:252-261, which passes a MockObject and expects 'Mock object string' via __str__().
  3. Diverges from the consistent pattern in sibling LLMs (e.g., flo_ai/flo_ai/llm/anthropic_llm.py:217-221) which fall back to str(response) for non-dict inputs.

Also note the declared type response: Dict[str, Any] is narrower than the actual accepted types — consider widening to Any to match the other LLM implementations and the stringly-typed MockObject case.

🐛 Proposed fix — restore `str(response)` fallback and align signature
-    def get_message_content(self, response: Dict[str, Any]) -> str:
-        if isinstance(response, str):
-            return response
-        if isinstance(response, dict):
-            content = response.get('content')
-            return str(content) if content is not None else ''
+    def get_message_content(self, response: Any) -> str:
+        if isinstance(response, str):
+            return response
+        if isinstance(response, dict):
+            return str(response.get('content', ''))
+        return str(response)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flo_ai/flo_ai/llm/gemini_llm.py` around lines 208 - 213, The
get_message_content function currently returns None for inputs that are neither
str nor dict; change its signature from response: Dict[str, Any] to response:
Any and ensure it always returns a str by adding a final fallback that returns
str(response) (or '' if response is None) so callers like agent.py (which call
self.llm.get_message_content(...).strip().upper()) and tests expecting __str__
behavior won't break; keep the existing handling for str and dict (use
response.get('content') -> str if present, else '') but ensure every code path
returns a string.


def format_tool_for_llm(self, tool: 'Tool') -> Dict[str, Any]:
"""Format a single tool for Gemini's function declarations"""
Expand Down
79 changes: 56 additions & 23 deletions wavefront/client/src/components/ChatBot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Switch } from '@app/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@app/components/ui/select';
import { Spinner } from '@app/components/ui/spinner';
import { Textarea } from '@app/components/ui/textarea';
import { Popover, PopoverContent, PopoverTrigger } from '@app/components/ui/popover';
import { LLMInferenceConfig } from '@app/types/llm-inference-config';
import { ChatMessageContent, ImageContent, DocumentContent } from '@app/types/chat-message';
import clsx from 'clsx';
Expand Down Expand Up @@ -106,6 +107,13 @@ const ChatBot = ({
const variablesModalRef = useRef<HTMLDivElement>(null);
const [showLogic, setShowLogic] = useState(true);
const [selectValue, setSelectValue] = useState<string>('');
const combinedAttachments = [
...uploadedImages.map((image, index) => ({ kind: 'image' as const, image, originalIndex: index })),
...uploadedDocuments.map((document, index) => ({ kind: 'document' as const, document, originalIndex: index })),
];
const visibleCombinedAttachments = combinedAttachments.slice(0, 2);
const remainingCombinedAttachmentsCount = Math.max(combinedAttachments.length - visibleCombinedAttachments.length, 0);
const remainingCombinedAttachments = combinedAttachments.slice(2);

return (
<div className="flex h-full w-full flex-col gap-7">
Expand Down Expand Up @@ -222,56 +230,81 @@ const ChatBot = ({

{/* Scrollable Attachments Container */}
{(uploadedImages.length > 0 || uploadedDocuments.length > 0) && (
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-x-auto">
{/* Uploaded Images */}
{uploadedImages.length > 0 && (
<div className="flex shrink-0 gap-2">
{uploadedImages.map((image, index) => (
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2 overflow-x-auto">
<div className="flex shrink-0 gap-2">
{visibleCombinedAttachments.map((attachment, index) =>
attachment.kind === 'image' ? (
<div
key={index}
className="group relative flex items-center gap-1 rounded-lg border border-gray-200 bg-white p-1 transition-colors hover:border-gray-300"
>
<img src={image.base64} alt={image.file.name} className="h-6 w-6 rounded object-cover" />
<img
src={attachment.image.base64}
alt={attachment.image.file.name}
className="h-6 w-6 rounded object-cover"
/>
<div className="flex flex-col">
<p className="max-w-[120px] truncate text-[8px] font-medium text-gray-800">{image.file.name}</p>
<p className="text-[8px] text-gray-500">{formatFileSize(image.file.size)}</p>
<p className="max-w-[120px] truncate text-[8px] font-medium text-gray-800">
{attachment.image.file.name}
</p>
<p className="text-[8px] text-gray-500">{formatFileSize(attachment.image.file.size)}</p>
</div>
<button
onClick={() => handleRemoveImage(index)}
onClick={() => handleRemoveImage(attachment.originalIndex)}
className="ml-2 text-red-500 transition-colors hover:text-red-700"
title="Remove image"
>
<X />
</button>
</div>
))}
</div>
)}

{/* Uploaded Documents */}
{uploadedDocuments.length > 0 && (
<div className="flex shrink-0 gap-2">
{uploadedDocuments.map((doc, index) => (
) : (
<div
key={index}
className="group relative flex items-center gap-2 rounded-lg border border-gray-200 bg-white p-2 transition-colors hover:border-gray-300"
>
<div className="text-gray-600">📄</div>
<div className="flex flex-col">
<p className="max-w-[120px] truncate text-[8px] font-medium text-gray-800">{doc.file.name}</p>
<p className="text-[8px] text-gray-500">{formatFileSize(doc.file.size)}</p>
<p className="max-w-[120px] truncate text-[8px] font-medium text-gray-800">
{attachment.document.file.name}
</p>
<p className="text-[8px] text-gray-500">{formatFileSize(attachment.document.file.size)}</p>
</div>
<button
onClick={() => handleRemoveDocument(index)}
onClick={() => handleRemoveDocument(attachment.originalIndex)}
className="ml-2 text-red-500 transition-colors hover:text-red-700"
title="Remove document"
>
<X />
</button>
</div>
))}
</div>
)}
)
)}
{remainingCombinedAttachmentsCount > 0 && (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="flex cursor-pointer items-center rounded-lg border border-gray-200 bg-white px-2 text-xs font-medium text-gray-600 transition-colors hover:border-gray-300"
title="Show remaining attachments"
>
+{remainingCombinedAttachmentsCount}
</button>
</PopoverTrigger>
<PopoverContent align="start" className="w-64 bg-gray-800 p-2">
<div className="b flex flex-col gap-1.5 opacity-80">
Comment on lines +293 to +294
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Stray b class in popover content.

className="b flex flex-col gap-1.5 opacity-80" contains a leftover b class that isn't a valid Tailwind utility and appears to be a typo.

🧹 Proposed fix
-                    <PopoverContent align="start" className="w-64 bg-gray-800 p-2">
-                      <div className="b flex flex-col gap-1.5 opacity-80">
+                    <PopoverContent align="start" className="w-64 bg-gray-800 p-2">
+                      <div className="flex flex-col gap-1.5 opacity-80">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PopoverContent align="start" className="w-64 bg-gray-800 p-2">
<div className="b flex flex-col gap-1.5 opacity-80">
<PopoverContent align="start" className="w-64 bg-gray-800 p-2">
<div className="flex flex-col gap-1.5 opacity-80">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wavefront/client/src/components/ChatBot.tsx` around lines 293 - 294, In
ChatBot.tsx, remove the stray "b" token from the PopoverContent className (the
JSX element using PopoverContent with className="b flex flex-col gap-1.5
opacity-80"); update that className to only valid Tailwind utilities (e.g.,
"flex flex-col gap-1.5 opacity-80") so there are no invalid/typo classes left in
the PopoverContent element.

{remainingCombinedAttachments.map((attachment, index) => (
<p
key={`${attachment.kind}-${attachment.originalIndex}-${index}`}
className="truncate text-xs text-white"
>
{attachment.kind === 'image' ? attachment.image.file.name : attachment.document.file.name}
</p>
))}
</div>
</PopoverContent>
</Popover>
)}
</div>
</div>
)}
</div>
Expand Down
2 changes: 1 addition & 1 deletion wavefront/client/src/pages/apps/[appId]/workflows/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ const WorkflowDetail: React.FC = () => {
<DialogHeader>
<DialogTitle>Edit Workflow Configuration</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-3 py-4">
<div className="flex flex-col gap-3 overflow-auto py-4">
<CodeMirror
value={yamlContent}
editable={true}
Expand Down
2 changes: 1 addition & 1 deletion wavefront/server/modules/agents_module/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies = [
"flo-utils",
"tools-module",
"api-services-module",
"flo-ai==1.1.3",
"flo-ai==1.1.4",
]

[tool.uv.sources]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dependencies = [
"pandas~=2.2.3",
"ollama~=0.4.8",
"textract~=1.6.5",
"flo-ai==1.1.3",
"flo-ai==1.1.4",
"google-cloud-pubsub~=2.30.0",
"boto3<=1.38.40",
"pyyaml>=6.0.3,<7",
Expand Down
2 changes: 1 addition & 1 deletion wavefront/server/modules/tools_module/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "tools_module"
version = "0.1.0"
description = "Tools module for Flo AI agent system"
dependencies = [
"flo-ai==1.1.3",
"flo-ai==1.1.4",
"flo_cloud",

"datasource",
Expand Down
62 changes: 20 additions & 42 deletions wavefront/server/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading