mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(rnd): Add initial block execution credit accounting UI on AutoGPT Builder (#8078)
This commit is contained in:
32
rnd/autogpt_builder/src/components/CreditButton.tsx
Normal file
32
rnd/autogpt_builder/src/components/CreditButton.tsx
Normal 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>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 ===== ##
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user