mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
Compare commits
6 Commits
fix/artifa
...
abhi/add-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35d27ece4a | ||
|
|
3e6be0c749 | ||
|
|
3969a1a29a | ||
|
|
e286fb6990 | ||
|
|
afe2257609 | ||
|
|
516097469e |
@@ -464,6 +464,9 @@ async def create_library_agent(
|
||||
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
||||
).model_dump()
|
||||
),
|
||||
topIntegrations=SafeJson(
|
||||
library_model._compute_top_integrations(graph_entry)
|
||||
),
|
||||
**(
|
||||
{"Folder": {"connect": {"id": folder_id}}}
|
||||
if folder_id and graph_entry is graph
|
||||
@@ -481,6 +484,9 @@ async def create_library_agent(
|
||||
sensitive_action_safe_mode=sensitive_action_safe_mode,
|
||||
).model_dump()
|
||||
),
|
||||
"topIntegrations": SafeJson(
|
||||
library_model._compute_top_integrations(graph_entry)
|
||||
),
|
||||
**(
|
||||
{"Folder": {"connect": {"id": folder_id}}}
|
||||
if folder_id and graph_entry is graph
|
||||
@@ -652,6 +658,15 @@ async def update_library_agent_version_and_settings(
|
||||
user_id=user_id,
|
||||
settings=updated_settings,
|
||||
)
|
||||
|
||||
# Recompute top integrations on version update
|
||||
top_integrations = library_model._compute_top_integrations(agent_graph)
|
||||
await prisma.models.LibraryAgent.prisma().update(
|
||||
where={"id": library.id},
|
||||
data={"topIntegrations": SafeJson(top_integrations)},
|
||||
)
|
||||
library.top_integrations = top_integrations
|
||||
|
||||
return library
|
||||
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ async def test_get_library_agents(mocker):
|
||||
userId="test-user",
|
||||
agentGraphId="agent2",
|
||||
settings="{}", # type: ignore
|
||||
topIntegrations="[]", # type: ignore
|
||||
agentGraphVersion=1,
|
||||
isCreatedByUser=False,
|
||||
isDeleted=False,
|
||||
@@ -121,6 +122,7 @@ async def test_add_agent_to_library(mocker):
|
||||
userId="test-user",
|
||||
agentGraphId=mock_store_listing_data.agentGraphId,
|
||||
settings="{}", # type: ignore
|
||||
topIntegrations="[]", # type: ignore
|
||||
agentGraphVersion=1,
|
||||
isCreatedByUser=False,
|
||||
isDeleted=False,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import collections
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
@@ -6,6 +7,7 @@ import prisma.enums
|
||||
import prisma.models
|
||||
import pydantic
|
||||
|
||||
from backend.blocks._base import BlockCategory
|
||||
from backend.data.graph import GraphModel, GraphSettings, GraphTriggerInfo
|
||||
from backend.data.model import (
|
||||
CredentialsMetaInput,
|
||||
@@ -144,6 +146,13 @@ class RecentExecution(pydantic.BaseModel):
|
||||
activity_summary: str | None = None
|
||||
|
||||
|
||||
def _parse_top_integrations(raw: object, graph: GraphModel) -> list[dict[str, str]]:
|
||||
"""Parse topIntegrations from database, falling back to on-the-fly computation."""
|
||||
if raw and isinstance(raw, list) and len(raw) > 0:
|
||||
return [dict(item) for item in raw]
|
||||
return _compute_top_integrations(graph)
|
||||
|
||||
|
||||
def _parse_settings(settings: dict | str | None) -> GraphSettings:
|
||||
"""Parse settings from database, handling both dict and string formats."""
|
||||
if settings is None:
|
||||
@@ -156,6 +165,62 @@ def _parse_settings(settings: dict | str | None) -> GraphSettings:
|
||||
return GraphSettings()
|
||||
|
||||
|
||||
# Priority order for category-based integration entries
|
||||
_CATEGORY_PRIORITY: list[BlockCategory] = [
|
||||
BlockCategory.AI,
|
||||
BlockCategory.SOCIAL,
|
||||
BlockCategory.COMMUNICATION,
|
||||
BlockCategory.DEVELOPER_TOOLS,
|
||||
BlockCategory.DATA,
|
||||
BlockCategory.CRM,
|
||||
BlockCategory.PRODUCTIVITY,
|
||||
BlockCategory.ISSUE_TRACKING,
|
||||
BlockCategory.TEXT,
|
||||
BlockCategory.SEARCH,
|
||||
BlockCategory.MULTIMEDIA,
|
||||
BlockCategory.MARKETING,
|
||||
BlockCategory.LOGIC,
|
||||
BlockCategory.BASIC,
|
||||
BlockCategory.INPUT,
|
||||
BlockCategory.OUTPUT,
|
||||
]
|
||||
|
||||
|
||||
def _compute_top_integrations(
|
||||
graph: GraphModel,
|
||||
) -> list[dict[str, str]]:
|
||||
"""Compute the top integrations used by an agent's graph.
|
||||
|
||||
Returns up to 5 entries: providers first (by frequency), then categories.
|
||||
"""
|
||||
provider_counter: collections.Counter[str] = collections.Counter()
|
||||
category_counter: collections.Counter[BlockCategory] = collections.Counter()
|
||||
|
||||
for g in [graph, *graph.sub_graphs]:
|
||||
for node in g.nodes:
|
||||
for info in node.block.input_schema.get_credentials_fields_info().values():
|
||||
for provider in info.provider:
|
||||
provider_counter[provider] += 1
|
||||
|
||||
if node.block.categories:
|
||||
for cat in node.block.categories:
|
||||
category_counter[cat] += 1
|
||||
|
||||
result: list[dict[str, str]] = [
|
||||
{"name": name, "type": "provider"}
|
||||
for name, _ in provider_counter.most_common(5)
|
||||
]
|
||||
|
||||
if len(result) < 5:
|
||||
for cat in _CATEGORY_PRIORITY:
|
||||
if len(result) >= 5:
|
||||
break
|
||||
if category_counter.get(cat, 0) > 0:
|
||||
result.append({"name": cat.name, "type": "category"})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class LibraryAgent(pydantic.BaseModel):
|
||||
"""
|
||||
Represents an agent in the library, including metadata for display and
|
||||
@@ -215,6 +280,7 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
|
||||
recommended_schedule_cron: str | None = None
|
||||
settings: GraphSettings = pydantic.Field(default_factory=GraphSettings)
|
||||
top_integrations: list[dict[str, str]] = pydantic.Field(default_factory=list)
|
||||
marketplace_listing: Optional["MarketplaceListing"] = None
|
||||
|
||||
@staticmethod
|
||||
@@ -355,6 +421,7 @@ class LibraryAgent(pydantic.BaseModel):
|
||||
folder_name=agent.Folder.name if agent.Folder else None,
|
||||
recommended_schedule_cron=agent.AgentGraph.recommendedScheduleCron,
|
||||
settings=_parse_settings(agent.settings),
|
||||
top_integrations=_parse_top_integrations(agent.topIntegrations, graph),
|
||||
marketplace_listing=marketplace_listing_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ async def test_get_library_agents_success(
|
||||
assert data.agents[1].can_access_graph is False
|
||||
|
||||
snapshot.snapshot_dir = "snapshots"
|
||||
snapshot.assert_match(json.dumps(response.json(), indent=2), "lib_agts_search")
|
||||
snapshot.assert_match(f"{json.dumps(response.json(), indent=2)}\n", "lib_agts_search")
|
||||
|
||||
mock_db_call.assert_called_once_with(
|
||||
user_id=test_user_id,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "LibraryAgent" ADD COLUMN "topIntegrations" JSONB NOT NULL DEFAULT '[]';
|
||||
@@ -428,6 +428,8 @@ model LibraryAgent {
|
||||
|
||||
settings Json @default("{}")
|
||||
|
||||
topIntegrations Json @default("[]")
|
||||
|
||||
@@unique([userId, agentGraphId, agentGraphVersion])
|
||||
@@index([agentGraphId, agentGraphVersion])
|
||||
@@index([creatorId])
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"human_in_the_loop_safe_mode": true,
|
||||
"sensitive_action_safe_mode": false
|
||||
},
|
||||
"top_integrations": [],
|
||||
"marketplace_listing": null
|
||||
},
|
||||
{
|
||||
@@ -90,6 +91,7 @@
|
||||
"human_in_the_loop_safe_mode": true,
|
||||
"sensitive_action_safe_mode": false
|
||||
},
|
||||
"top_integrations": [],
|
||||
"marketplace_listing": null
|
||||
}
|
||||
],
|
||||
@@ -99,4 +101,4 @@
|
||||
"current_page": 1,
|
||||
"page_size": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import Avatar, {
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { Link } from "@/components/atoms/Link/Link";
|
||||
import { IntegrationLinkImage } from "@/components/molecules/IntegrationLinkImage/IntegrationLinkImage";
|
||||
import { AgentCardMenu } from "./components/AgentCardMenu";
|
||||
import { FavoriteButton } from "./components/FavoriteButton";
|
||||
import { useLibraryAgentCard } from "./useLibraryAgentCard";
|
||||
@@ -102,20 +103,17 @@ export function LibraryAgentCard({ agent, draggable = true }: Props) {
|
||||
</Text>
|
||||
|
||||
{!image_url ? (
|
||||
<div
|
||||
className={`h-[3.64rem] w-[6.70rem] flex-shrink-0 rounded-small ${
|
||||
[
|
||||
"bg-gradient-to-r from-green-200 to-blue-200",
|
||||
"bg-gradient-to-r from-pink-200 to-purple-200",
|
||||
"bg-gradient-to-r from-yellow-200 to-orange-200",
|
||||
"bg-gradient-to-r from-blue-200 to-cyan-200",
|
||||
"bg-gradient-to-r from-indigo-200 to-purple-200",
|
||||
][parseInt(id.slice(0, 8), 16) % 5]
|
||||
}`}
|
||||
style={{
|
||||
backgroundSize: "200% 200%",
|
||||
animation: "gradient 15s ease infinite",
|
||||
}}
|
||||
<IntegrationLinkImage
|
||||
integrations={
|
||||
"top_integrations" in agent
|
||||
? (agent.top_integrations as Array<{
|
||||
name: string;
|
||||
type: "provider" | "category";
|
||||
}>)
|
||||
: []
|
||||
}
|
||||
size="sm"
|
||||
className="h-[3.64rem] w-[6.70rem] flex-shrink-0 rounded-small"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import Image from "next/image";
|
||||
import { useRef, useState } from "react";
|
||||
import { AddToLibraryButton } from "../AddToLibraryButton/AddToLibraryButton";
|
||||
import { IntegrationLinkImage } from "@/components/molecules/IntegrationLinkImage/IntegrationLinkImage";
|
||||
|
||||
interface Props {
|
||||
agent: StoreAgent;
|
||||
@@ -62,7 +63,18 @@ export function FeaturedAgentCard({ agent, backgroundColor }: Props) {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 rounded-xl bg-violet-50" />
|
||||
<IntegrationLinkImage
|
||||
integrations={
|
||||
"top_integrations" in agent
|
||||
? (agent.top_integrations as Array<{
|
||||
name: string;
|
||||
type: "provider" | "category";
|
||||
}>)
|
||||
: []
|
||||
}
|
||||
size="lg"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { IntegrationLinkImage } from "@/components/molecules/IntegrationLinkImage/IntegrationLinkImage";
|
||||
import { OverflowText } from "@/components/atoms/OverflowText/OverflowText";
|
||||
import { Skeleton } from "@/components/atoms/Skeleton/Skeleton";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
@@ -25,6 +26,7 @@ interface Props {
|
||||
creatorSlug?: string;
|
||||
agentSlug?: string;
|
||||
agentGraphID?: string;
|
||||
topIntegrations?: Array<{ name: string; type: "provider" | "category" }>;
|
||||
}
|
||||
|
||||
export function StoreCard({
|
||||
@@ -40,6 +42,7 @@ export function StoreCard({
|
||||
creatorSlug,
|
||||
agentSlug,
|
||||
agentGraphID,
|
||||
topIntegrations,
|
||||
}: Props) {
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
@@ -79,9 +82,10 @@ export function StoreCard({
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="absolute inset-0 rounded-xl"
|
||||
style={{ backgroundColor: "rgb(216, 208, 255)" }}
|
||||
<IntegrationLinkImage
|
||||
integrations={topIntegrations ?? []}
|
||||
size="md"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10311,6 +10311,14 @@
|
||||
"title": "Recommended Schedule Cron"
|
||||
},
|
||||
"settings": { "$ref": "#/components/schemas/GraphSettings" },
|
||||
"top_integrations": {
|
||||
"items": {
|
||||
"additionalProperties": { "type": "string" },
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array",
|
||||
"title": "Top Integrations"
|
||||
},
|
||||
"marketplace_listing": {
|
||||
"anyOf": [
|
||||
{ "$ref": "#/components/schemas/MarketplaceListing" },
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
getCategoryIcon,
|
||||
getProviderIconPath,
|
||||
RobotIcon,
|
||||
PlugIcon,
|
||||
} from "./helpers";
|
||||
|
||||
interface Props {
|
||||
integrations: Array<{ name: string; type: "provider" | "category" }>;
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SIZE_CONFIG = {
|
||||
sm: { icon: 20, gap: 8, lineWidth: 12 },
|
||||
md: { icon: 28, gap: 12, lineWidth: 16 },
|
||||
lg: { icon: 36, gap: 16, lineWidth: 20 },
|
||||
} as const;
|
||||
|
||||
function ProviderIcon({ name, iconSize }: { name: string; iconSize: number }) {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
if (hasError) {
|
||||
return <PlugIcon size={iconSize} className="text-zinc-400" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
src={getProviderIconPath(name)}
|
||||
alt={name}
|
||||
width={iconSize}
|
||||
height={iconSize}
|
||||
className="rounded-sm object-contain"
|
||||
onError={() => setHasError(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectingLine({ width, height }: { width: number; height: number }) {
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
<line
|
||||
x1={0}
|
||||
y1={height / 2}
|
||||
x2={width}
|
||||
y2={height / 2}
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
className="text-zinc-300"
|
||||
/>
|
||||
<polygon
|
||||
points={`${width - 4},${height / 2 - 3} ${width},${height / 2} ${width - 4},${height / 2 + 3}`}
|
||||
fill="currentColor"
|
||||
className="text-zinc-300"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IntegrationLinkImage({
|
||||
integrations,
|
||||
size = "sm",
|
||||
className = "",
|
||||
}: Props) {
|
||||
const config = SIZE_CONFIG[size];
|
||||
const items = integrations.slice(0, 3);
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-small bg-zinc-50 ${className}`}
|
||||
>
|
||||
<RobotIcon size={config.icon} className="text-zinc-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-center gap-0 rounded-small bg-zinc-50 ${className}`}
|
||||
>
|
||||
{items.map((item, i) => (
|
||||
<div key={`${item.name}-${i}`} className="flex items-center">
|
||||
{i > 0 && (
|
||||
<ConnectingLine width={config.lineWidth} height={config.icon} />
|
||||
)}
|
||||
<div className="flex items-center justify-center">
|
||||
{item.type === "provider" ? (
|
||||
<ProviderIcon name={item.name} iconSize={config.icon} />
|
||||
) : (
|
||||
(() => {
|
||||
const CategoryIcon = getCategoryIcon(item.name);
|
||||
return (
|
||||
<CategoryIcon size={config.icon} className="text-zinc-500" />
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
BrainIcon,
|
||||
UsersThreeIcon,
|
||||
ChatCircleIcon,
|
||||
CodeIcon,
|
||||
DatabaseIcon,
|
||||
TextTIcon,
|
||||
MagnifyingGlassIcon,
|
||||
GitBranchIcon,
|
||||
CubeIcon,
|
||||
ArrowSquareInIcon,
|
||||
ArrowSquareOutIcon,
|
||||
AddressBookIcon,
|
||||
FilmStripIcon,
|
||||
CheckSquareIcon,
|
||||
MegaphoneIcon,
|
||||
BugIcon,
|
||||
RobotIcon,
|
||||
PlugIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import type { Icon } from "@phosphor-icons/react";
|
||||
|
||||
const CATEGORY_ICON_MAP: Record<string, Icon> = {
|
||||
AI: BrainIcon,
|
||||
SOCIAL: UsersThreeIcon,
|
||||
COMMUNICATION: ChatCircleIcon,
|
||||
DEVELOPER_TOOLS: CodeIcon,
|
||||
DATA: DatabaseIcon,
|
||||
TEXT: TextTIcon,
|
||||
SEARCH: MagnifyingGlassIcon,
|
||||
LOGIC: GitBranchIcon,
|
||||
BASIC: CubeIcon,
|
||||
INPUT: ArrowSquareInIcon,
|
||||
OUTPUT: ArrowSquareOutIcon,
|
||||
CRM: AddressBookIcon,
|
||||
MULTIMEDIA: FilmStripIcon,
|
||||
PRODUCTIVITY: CheckSquareIcon,
|
||||
MARKETING: MegaphoneIcon,
|
||||
ISSUE_TRACKING: BugIcon,
|
||||
};
|
||||
|
||||
export function getCategoryIcon(categoryName: string): Icon {
|
||||
return CATEGORY_ICON_MAP[categoryName] ?? CubeIcon;
|
||||
}
|
||||
|
||||
export function getProviderIconPath(providerName: string): string {
|
||||
return `/integrations/${providerName}.png`;
|
||||
}
|
||||
|
||||
export { RobotIcon, PlugIcon };
|
||||
@@ -525,6 +525,7 @@ export type LibraryAgent = {
|
||||
is_favorite: boolean;
|
||||
is_latest_version: boolean;
|
||||
recommended_schedule_cron: string | null;
|
||||
top_integrations: Array<{ name: string; type: "provider" | "category" }>;
|
||||
} & (
|
||||
| {
|
||||
has_external_trigger: true;
|
||||
|
||||
Reference in New Issue
Block a user