feat(rnd): Add initial block execution credit accounting UI on AutoGPT Builder (#8078)

This commit is contained in:
Zamil Majdy
2024-09-18 16:21:40 -05:00
committed by GitHub
parent 9b5bf81d7c
commit c3cb90ac20
14 changed files with 144 additions and 16 deletions

View File

@@ -0,0 +1,32 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { IconRefresh } from "@/components/ui/icons";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
export default function CreditButton() {
const [credit, setCredit] = useState<number | null>(null);
const api = new AutoGPTServerAPI();
const fetchCredit = async () => {
const response = await api.getUserCredit();
setCredit(response.credits);
};
useEffect(() => {
fetchCredit();
}, [api]);
return (
credit !== null && (
<Button
onClick={fetchCredit}
variant="outline"
className="flex items-center space-x-2 text-muted-foreground"
>
<span>Credits: {credit}</span>
<IconRefresh />
</Button>
)
);
}

View File

@@ -16,6 +16,7 @@ import {
Category,
NodeExecutionResult,
BlockUIType,
BlockCost,
} from "@/lib/autogpt-server-api/types";
import { beautifyString, cn, setNestedProperty } from "@/lib/utils";
import { Button } from "@/components/ui/button";
@@ -45,6 +46,7 @@ export type ConnectionData = Array<{
export type CustomNodeData = {
blockType: string;
blockCosts: BlockCost[];
title: string;
description: string;
categories: Category[];
@@ -521,6 +523,18 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
);
});
const inputValues = data.hardcodedValues;
const blockCost =
data.blockCosts &&
data.blockCosts.find((cost) =>
Object.entries(cost.cost_filter).every(
// Undefined, null, or empty values are considered equal
([key, value]) =>
value === inputValues[key] || (!value && !inputValues[key]),
),
);
console.debug(`Block cost ${inputValues}|${data.blockCosts}=${blockCost}`);
return (
<div
className={`${data.uiType === BlockUIType.NOTE ? "w-[300px]" : "w-[500px]"} ${blockClasses} ${errorClass} ${statusClass} ${data.uiType === BlockUIType.NOTE ? "bg-yellow-100" : "bg-white"}`}
@@ -562,6 +576,11 @@ export function CustomNode({ data, id, width, height }: NodeProps<CustomNode>) {
)}
</div>
</div>
{blockCost && (
<div className="p-3 text-right font-semibold">
Cost: {blockCost.cost_amount} / {blockCost.cost_type}
</div>
)}
{data.uiType !== BlockUIType.NOTE ? (
<div className="flex items-start justify-between p-3">
<div>

View File

@@ -414,6 +414,7 @@ const FlowEditor: React.FC<{
position: viewportCenter, // Set the position to the calculated viewport center
data: {
blockType: nodeType,
blockCosts: nodeSchema.costs,
title: `${nodeType} ${nodeId}`,
description: nodeSchema.description,
categories: nodeSchema.categories,

View File

@@ -9,9 +9,12 @@ import {
IconCircleUser,
IconMenu,
IconPackage2,
IconRefresh,
IconSquareActivity,
IconWorkFlow,
} from "@/components/ui/icons";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import CreditButton from "@/components/CreditButton";
export async function NavBar() {
const isAvailable = Boolean(
@@ -96,6 +99,8 @@ export async function NavBar() {
</a>
</div>
<div className="flex flex-1 items-center justify-end gap-4">
{isAvailable && user && <CreditButton />}
{isAvailable && !user && (
<Link
href="/login"

View File

@@ -264,6 +264,43 @@ export const IconCircleUser = createIcon((props) => (
</svg>
));
/**
* Refresh icon component.
*
* @component IconRefresh
* @param {IconProps} props - The props object containing additional attributes and event handlers for the icon.
* @returns {JSX.Element} - The refresh icon.
*
* @example
* // Default usage this is the standard usage
* <IconRefresh />
*
* @example
* // With custom color and size these should be used sparingly and only when necessary
* <IconRefresh className="text-primary" size="lg" />
*
* @example
* // With custom size and onClick handler
* <IconRefresh size="sm" onClick={handleOnClick} />
*/
export const IconRefresh = createIcon((props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<polyline points="23 4 23 10 17 10" />
<polyline points="1 20 1 14 7 14" />
<path d="M3.51 9a9 9 0 0 1 14.136 -5.36L23 10" />
<path d="M20.49 15a9 9 0 0 1 -14.136 5.36L1 14" />
</svg>
));
/**
* Menu icon component.
*

View File

@@ -145,6 +145,7 @@ export default function useAgentGraph(
data: {
block_id: block.id,
blockType: block.name,
blockCosts: block.costs,
categories: block.categories,
description: block.description,
title: `${block.name} ${node.id}`,

View File

@@ -36,6 +36,10 @@ export default class BaseAutoGPTServerAPI {
return this._request("POST", "/auth/user", {});
}
async getUserCredit(): Promise<{ credits: number }> {
return this._get(`/credits`);
}
async getBlocks(): Promise<Block[]> {
return await this._get("/blocks");
}

View File

@@ -5,6 +5,18 @@ export type Category = {
description: string;
};
export enum BlockCostType {
RUN = "run",
BYTE = "byte",
SECOND = "second",
}
export type BlockCost = {
cost_amount: number;
cost_type: BlockCostType;
cost_filter: { [key: string]: any };
};
export type Block = {
id: string;
name: string;
@@ -14,6 +26,7 @@ export type Block = {
outputSchema: BlockIORootSchema;
staticOutput: boolean;
uiType: BlockUIType;
costs: BlockCost[];
};
export type BlockIORootSchema = {

View File

@@ -1,7 +1,7 @@
DB_USER=agpt_user
DB_PASS=pass123
DB_NAME=agpt_local
DB_PORT=5432
DB_PORT=5433
DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@localhost:${DB_PORT}/${DB_NAME}"
PRISMA_SCHEMA="postgres/schema.prisma"
@@ -14,6 +14,8 @@ ENABLE_CREDIT=false
APP_ENV="local"
PYRO_HOST=localhost
SENTRY_DSN=
# This is needed when ENABLE_AUTH is true
SUPABASE_JWT_SECRET=
## ===== OPTIONAL API KEYS ===== ##

View File

@@ -15,6 +15,7 @@ from autogpt_server.blocks.llm import (
AIStructuredResponseGeneratorBlock,
AITextGeneratorBlock,
AITextSummarizerBlock,
LlmModel,
)
from autogpt_server.blocks.talking_head import CreateTalkingAvatarVideoBlock
from autogpt_server.data.block import Block, BlockInput
@@ -57,6 +58,12 @@ llm_cost = [
cost_amount=metadata.cost_factor,
)
for model, metadata in MODEL_METADATA.items()
] + [
BlockCost(
# Default cost is running LlmModel.GPT4O.
cost_amount=MODEL_METADATA[LlmModel.GPT4O].cost_factor,
cost_filter={"api_key": None},
),
]
BLOCK_COSTS: dict[Type[Block], list[BlockCost]] = {
@@ -175,7 +182,11 @@ class UserCredit(UserCreditBase):
return 0, {}
for block_cost in block_costs:
if all(input_data.get(k) == b for k, b in block_cost.cost_filter.items()):
if all(
# None, [], {}, "", are considered the same value.
input_data.get(k) == b or (not input_data.get(k) and not b)
for k, b in block_cost.cost_filter.items()
):
if block_cost.cost_type == BlockCostType.RUN:
return block_cost.cost_amount, block_cost.cost_filter

View File

@@ -108,11 +108,6 @@ class AgentServer(AppService):
methods=["GET"],
tags=["blocks"],
)
api_router.add_api_route(
path="/blocks/costs",
endpoint=self.get_graph_block_costs,
methods=["GET"],
)
api_router.add_api_route(
path="/blocks/{block_id}/execute",
endpoint=self.execute_graph_block,
@@ -256,7 +251,7 @@ class AgentServer(AppService):
app.include_router(api_router)
uvicorn.run(app, host="0.0.0.0", port=8000, log_config=None)
uvicorn.run(app, host="0.0.0.0", port=Config().agent_api_port, log_config=None)
def set_test_dependency_overrides(self, overrides: dict):
self._test_dependency_overrides = overrides
@@ -312,11 +307,9 @@ class AgentServer(AppService):
@classmethod
def get_graph_blocks(cls) -> list[dict[Any, Any]]:
return [v.to_dict() for v in block.get_blocks().values()]
@classmethod
def get_graph_block_costs(cls) -> dict[Any, Any]:
return get_block_costs()
blocks = block.get_blocks()
costs = get_block_costs()
return [{**b.to_dict(), "costs": costs.get(b.id, [])} for b in blocks.values()]
@classmethod
def execute_graph_block(

View File

@@ -11,7 +11,7 @@ from autogpt_server.data.user import DEFAULT_USER_ID
from autogpt_server.server.conn_manager import ConnectionManager
from autogpt_server.server.model import ExecutionSubscription, Methods, WsMessage
from autogpt_server.util.service import AppProcess
from autogpt_server.util.settings import Settings
from autogpt_server.util.settings import Config, Settings
logger = logging.getLogger(__name__)
settings = Settings()
@@ -174,4 +174,4 @@ async def websocket_router(
class WebsocketServer(AppProcess):
def run(self):
uvicorn.run(app, host="0.0.0.0", port=8001)
uvicorn.run(app, host="0.0.0.0", port=Config().websocket_server_port)

View File

@@ -80,6 +80,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
extra="allow",
)
websocket_server_port: int = Field(
default=8001,
description="The port for the websocket server to run on",
)
execution_manager_port: int = Field(
default=8002,
description="The port for execution manager daemon to run on",
@@ -95,6 +100,11 @@ class Config(UpdateTrackingModel["Config"], BaseSettings):
description="The port for agent server daemon to run on",
)
agent_api_port: int = Field(
default=8006,
description="The port for agent server API to run on",
)
@classmethod
def settings_customise_sources(
cls,

View File

@@ -83,7 +83,7 @@ services:
- PYRO_HOST=0.0.0.0
- EXECUTIONMANAGER_HOST=executor
ports:
- "8006:8000"
- "8006:8006"
- "8003:8003" # execution scheduler
networks:
- app-network