Skip to content

Commit

Permalink
feat(rnd) Agent Marketplace MVP (#7657)
Browse files Browse the repository at this point in the history
* Added listing, sorting, filtering and ordering of agents

* feat(market): general upkeep for vscode and small docs

* feat(market): most of search

* fix(market): hinting on the sort was weird + linting

* feat(market): migrations and schema updates

* lint(market): autolint

* feat(market): better search

* feat(market): file download

* feat(market): analytics of downloads

* Added tracking of views

* changed all imports to be fully qualified

* Upgrade sentry sdk

* Added an admin endpoint to submit new agents

* fixes

* Added endpoint that just tracks download

* Starting adding the marketplace page

* Marketplace client

* Create template of the marketplace page

* Updated client

* fix(market): debug port

* feat(market): agents by downloads

* fix(market, builder): hook up frontend and backend

* feat(builder, market): build a "better" market page that loads data

* feat(builder): updated search (working) and page (kinda working)

* feat(builder): add a feature agents ui (not backed yet)

* feat(builder): improve detail page content

* Added run script

* Added pre populate database command

* Add AnalyticsTracker on create agent

* Add download counts for top agents

* Add hb page prometheus metrics

* Added featured agents funcitonality

* renamed endpoint to health

* Adding download flow

* normalised api routes

* update readme

* feat(market) : default featured

* formatting

* revert changes to autogpt and forge

* Updated Readme

* Eerror when creating an agent from a template installed from (#7697)

fix creating graph from template

* Add dockerfile

* z level fix

* Updated env vars

* updated populate url

* formatting

* fixed linting error

* Set defaults

* Allow only next.js dev server

* fixed url

* removed graph reassignment as due to change in master

---------

Co-authored-by: Nicholas Tindle <[email protected]>
  • Loading branch information
Swiftyos and ntindle authored Aug 5, 2024
1 parent 9ae6389 commit 951abf6
Show file tree
Hide file tree
Showing 37 changed files with 2,516 additions and 624 deletions.
6 changes: 5 additions & 1 deletion .vscode/all-projects.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
"name": "autogpt_builder",
"path": "../rnd/autogpt_builder"
},
{
"name": "market",
"path": "../rnd/market"
},
{
"name": "[root]",
"path": ".."
}
],
"settings": {
"python.analysis.typeCheckingMode": "basic",
"python.analysis.typeCheckingMode": "basic"
},
"extensions": {
"recommendations": [
Expand Down
3 changes: 2 additions & 1 deletion rnd/autogpt_builder/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
AGPT_SERVER_URL=http://localhost:8000/api
NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8000/api
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8001/api/v1/market

## Supabase credentials
## YOU ONLY NEED THEM IF YOU WANT TO USE SUPABASE USER AUTHENTICATION
Expand Down
4 changes: 3 additions & 1 deletion rnd/autogpt_builder/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ dotenv.config();
/** @type {import('next').NextConfig} */
const nextConfig = {
env: {
AGPT_SERVER_URL: process.env.AGPT_SERVER_URL,
NEXT_PUBLIC_AGPT_SERVER_URL: process.env.NEXT_PUBLIC_AGPT_SERVER_URL,
NEXT_PUBLIC_AGPT_MARKETPLACE_URL:
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL,
},
async redirects() {
return [
Expand Down
41 changes: 41 additions & 0 deletions rnd/autogpt_builder/src/app/marketplace/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Suspense } from "react";
import { notFound } from "next/navigation";
import MarketplaceAPI from "@/lib/marketplace-api";
import { AgentDetailResponse } from "@/lib/marketplace-api";
import AgentDetailContent from "@/components/AgentDetailContent";

async function getAgentDetails(id: string): Promise<AgentDetailResponse> {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8001/api/v1/market";
const api = new MarketplaceAPI(apiUrl);
try {
console.log(`Fetching agent details for id: ${id}`);
const agent = await api.getAgentDetails(id);
console.log(`Agent details fetched:`, agent);
return agent;
} catch (error) {
console.error(`Error fetching agent details:`, error);
throw error;
}
}

export default async function AgentDetailPage({
params,
}: {
params: { id: string };
}) {
let agent: AgentDetailResponse;

try {
agent = await getAgentDetails(params.id);
} catch (error) {
return notFound();
}

return (
<Suspense fallback={<div>Loading...</div>}>
<AgentDetailContent agent={agent} />
</Suspense>
);
}
293 changes: 293 additions & 0 deletions rnd/autogpt_builder/src/app/marketplace/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
"use client";
import React, { useEffect, useMemo, useState, useCallback } from "react";
import { useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import MarketplaceAPI, {
AgentResponse,
AgentListResponse,
AgentWithRank,
} from "@/lib/marketplace-api";
import { ChevronLeft, ChevronRight, Search, Star } from "lucide-react";

// Utility Functions
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}

// Types
type Agent = AgentResponse | AgentWithRank;

// Components
const HeroSection: React.FC = () => (
<div className="relative bg-indigo-600 py-6">
<div className="absolute inset-0 z-0">
<img
className="w-full h-full object-cover opacity-20"
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
alt="Marketplace background"
/>
<div
className="absolute inset-0 bg-indigo-600 mix-blend-multiply"
aria-hidden="true"
></div>
</div>
<div className="relative max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl">
AutoGPT Marketplace
</h1>
<p className="mt-2 max-w-3xl text-sm sm:text-base text-indigo-100">
Discover and share proven AI Agents to supercharge your business.
</p>
</div>
</div>
);

const SearchInput: React.FC<{
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}> = ({ value, onChange }) => (
<div className="mb-8 relative">
<Input
placeholder="Search agents..."
type="text"
className="w-full pl-10 pr-4 py-2 rounded-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
value={value}
onChange={onChange}
/>
<Search
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
size={20}
/>
</div>
);

const AgentCard: React.FC<{ agent: Agent; featured?: boolean }> = ({
agent,
featured = false,
}) => {
const router = useRouter();

const handleClick = () => {
router.push(`/marketplace/${agent.id}`);
};

return (
<div
className={`flex flex-col justify-between p-6 cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg border ${featured ? "border-indigo-500 shadow-md" : "border-gray-200"}`}
onClick={handleClick}
>
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{agent.name}
</h3>
{featured && <Star className="text-indigo-500" size={20} />}
</div>
<p className="text-sm text-gray-500 line-clamp-2 mb-4">
{agent.description}
</p>
<div className="text-xs text-gray-400 mb-2">
Categories: {agent.categories.join(", ")}
</div>
</div>
<div className="flex justify-between items-end">
<div className="text-xs text-gray-400">
Updated {new Date(agent.updatedAt).toLocaleDateString()}
</div>
<div className="text-xs text-gray-400">Downloads {agent.downloads}</div>
{"rank" in agent && (
<div className="text-xs text-indigo-600">
Rank: {agent.rank.toFixed(2)}
</div>
)}
</div>
</div>
);
};

const AgentGrid: React.FC<{
agents: Agent[];
title: string;
featured?: boolean;
}> = ({ agents, title, featured = false }) => (
<div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">{title}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{agents.map((agent) => (
<AgentCard agent={agent} key={agent.id} featured={featured} />
))}
</div>
</div>
);

const Pagination: React.FC<{
page: number;
totalPages: number;
onPrevPage: () => void;
onNextPage: () => void;
}> = ({ page, totalPages, onPrevPage, onNextPage }) => (
<div className="flex justify-between items-center mt-8">
<Button
onClick={onPrevPage}
disabled={page === 1}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<ChevronLeft size={16} />
<span>Previous</span>
</Button>
<span className="text-sm text-gray-700">
Page {page} of {totalPages}
</span>
<Button
onClick={onNextPage}
disabled={page === totalPages}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<span>Next</span>
<ChevronRight size={16} />
</Button>
</div>
);

// Main Component
const Marketplace: React.FC = () => {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8001/api/v1/market";
const api = useMemo(() => new MarketplaceAPI(apiUrl), [apiUrl]);

const [searchValue, setSearchValue] = useState("");
const [searchResults, setSearchResults] = useState<Agent[]>([]);
const [featuredAgents, setFeaturedAgents] = useState<Agent[]>([]);
const [topAgents, setTopAgents] = useState<Agent[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);

const fetchTopAgents = useCallback(
async (currentPage: number) => {
setIsLoading(true);
try {
const response = await api.getTopDownloadedAgents(currentPage, 9);
setTopAgents(response.agents);
setTotalPages(response.total_pages);
} catch (error) {
console.error("Error fetching top agents:", error);
} finally {
setIsLoading(false);
}
},
[api],
);

const fetchFeaturedAgents = useCallback(async () => {
try {
const featured = await api.getFeaturedAgents();
setFeaturedAgents(featured.agents);
} catch (error) {
console.error("Error fetching featured agents:", error);
}
}, [api]);

const searchAgents = useCallback(
async (searchTerm: string) => {
setIsLoading(true);
try {
const response = await api.searchAgents(searchTerm, 1, 30);
const filteredAgents = response.filter((agent) => agent.rank > 0);
setSearchResults(filteredAgents);
} catch (error) {
console.error("Error searching agents:", error);
} finally {
setIsLoading(false);
}
},
[api],
);

const debouncedSearch = useMemo(
() => debounce(searchAgents, 300),
[searchAgents],
);

useEffect(() => {
if (searchValue) {
debouncedSearch(searchValue);
} else {
fetchTopAgents(page);
}
}, [searchValue, page, debouncedSearch, fetchTopAgents]);

useEffect(() => {
fetchFeaturedAgents();
}, [fetchFeaturedAgents]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
setPage(1);
};

const handleNextPage = () => {
if (page < totalPages) {
setPage(page + 1);
}
};

const handlePrevPage = () => {
if (page > 1) {
setPage(page - 1);
}
};

return (
<div className="bg-gray-50 min-h-screen">
<HeroSection />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<SearchInput value={searchValue} onChange={handleInputChange} />
{isLoading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
<p className="mt-2 text-gray-600">Loading agents...</p>
</div>
) : searchValue ? (
searchResults.length > 0 ? (
<AgentGrid agents={searchResults} title="Search Results" />
) : (
<div className="text-center py-12">
<p className="text-gray-600">
No agents found matching your search criteria.
</p>
</div>
)
) : (
<>
{featuredAgents.length > 0 && (
<AgentGrid
agents={featuredAgents}
title="Featured Agents"
featured={true}
/>
)}
<AgentGrid agents={topAgents} title="Top Downloaded Agents" />
<Pagination
page={page}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
</>
)}
</div>
</div>
);
};

export default Marketplace;
Loading

0 comments on commit 951abf6

Please sign in to comment.