Add Deploy view in AGS (#4756)

* update version, fix component factory bug

* add basic structure for deploy

* minor fixes, deploy v1

* minor text updated

* format fixes

* formatting fixes .. webby test samples

* update cli command, update views,

* packakge.json and other fixes

* format fixes
This commit is contained in:
Victor Dibia 2024-12-18 18:57:11 -08:00 committed by GitHub
parent c21555290e
commit 4e3a70303d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1328 additions and 628 deletions

View File

@ -52,14 +52,14 @@ Have a local model server like Ollama, vLLM or LMStudio that provide an OpenAI c
"component_type": "model",
"model_capabilities": {
"vision": false,
"function_calling": false,
"function_calling": true,
"json_output": false
}
}
```
```{caution}
It is important that you add the `model_capabilities` field to the model client specification for custom models. This is used by the framework instantiate and use the model correctly.
It is important that you add the `model_capabilities` field to the model client specification for custom models. This is used by the framework instantiate and use the model correctly. Also, the `AssistantAgent` and many other agents in AgentChat require the model to have the `function_calling` capability.
```
## Q: The server starts but I can't access the UI

View File

@ -29,8 +29,8 @@ from autogen_agentchat.messages import (
MultiModalMessage,
StopMessage,
TextMessage,
ToolCallRequestEvent,
ToolCallExecutionEvent,
ToolCallRequestEvent,
)
from autogen_core import CancellationToken, FunctionCall
from autogen_core.models._types import FunctionExecutionResult

View File

@ -54,7 +54,7 @@ def ui(
@app.command()
def serve(
workflow: str = "",
team: str = "",
host: str = "127.0.0.1",
port: int = 8084,
workers: int = 1,
@ -64,9 +64,9 @@ def serve(
Serve an API Endpoint based on an AutoGen Studio workflow json file.
Args:
workflow (str): Path to the workflow json file.
team (str): Path to the team json file.
host (str, optional): Host to run the UI on. Defaults to 127.0.0.1 (localhost).
port (int, optional): Port to run the UI on. Defaults to 8081.
port (int, optional): Port to run the UI on. Defaults to 8084
workers (int, optional): Number of workers to run the UI with. Defaults to 1.
reload (bool, optional): Whether to reload the UI on code changes. Defaults to False.
docs (bool, optional): Whether to generate API docs. Defaults to False.
@ -74,7 +74,11 @@ def serve(
"""
os.environ["AUTOGENSTUDIO_API_DOCS"] = str(docs)
os.environ["AUTOGENSTUDIO_WORKFLOW_FILE"] = workflow
os.environ["AUTOGENSTUDIO_TEAM_FILE"] = team
# validate the team file
if not os.path.exists(team):
raise ValueError(f"Team file not found: {team}")
uvicorn.run(
"autogenstudio.web.serve:app",

View File

@ -326,21 +326,19 @@ class ComponentFactory:
async def load_agent(self, config: AgentConfig, input_func: Optional[Callable] = None) -> AgentComponent:
"""Create agent instance from configuration."""
system_message = config.system_message if config.system_message else "You are a helpful assistant"
model_client = None
system_message = None
tools = []
if hasattr(config, "system_message") and config.system_message:
system_message = config.system_message
if hasattr(config, "model_client") and config.model_client:
model_client = await self.load(config.model_client)
if hasattr(config, "tools") and config.tools:
for tool_config in config.tools:
tool = await self.load(tool_config)
tools.append(tool)
try:
# Load model client if specified
model_client = None
if config.model_client:
model_client = await self.load(config.model_client)
# Load tools if specified
tools = []
if config.tools:
for tool_config in config.tools:
tool = await self.load(tool_config)
tools.append(tool)
if config.agent_type == AgentTypes.USERPROXY:
return UserProxyAgent(
name=config.name,

View File

@ -12,8 +12,8 @@ from autogen_agentchat.messages import (
MultiModalMessage,
StopMessage,
TextMessage,
ToolCallRequestEvent,
ToolCallExecutionEvent,
ToolCallRequestEvent,
)
from autogen_core import CancellationToken
from autogen_core import Image as AGImage

View File

@ -6,23 +6,23 @@ import os
from fastapi import FastAPI
from ..datamodel import Response
from ..workflowmanager import WorkflowManager
from ..teammanager import TeamManager
app = FastAPI()
workflow_file_path = os.environ.get("AUTOGENSTUDIO_WORKFLOW_FILE", None)
team_file_path = os.environ.get("AUTOGENSTUDIO_TEAM_FILE", None)
if workflow_file_path:
workflow_manager = WorkflowManager(workflow=workflow_file_path)
if team_file_path:
team_manager = TeamManager()
else:
raise ValueError("Workflow file must be specified")
raise ValueError("Team file must be specified")
@app.get("/predict/{task}")
async def predict(task: str):
response = Response(message="Task successfully completed", status=True, data=None)
try:
result_message = workflow_manager.run(message=task, clear_history=False)
result_message = await team_manager.run(task=task, team_config=team_file_path)
response.data = result_message
except Exception as e:
response.message = str(e)

View File

@ -42,6 +42,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1",
"tailwindcss": "^3.4.14",
"yarn": "^1.22.22",
"zustand": "^5.0.1"
@ -51,7 +52,7 @@
"@types/node": "^22.9.0",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/react-syntax-highlighter": "^15.5.10",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"typescript": "^5.3.3"
}

View File

@ -10,6 +10,7 @@ import {
PanelLeftClose,
PanelLeftOpen,
GalleryHorizontalEnd,
Rocket,
} from "lucide-react";
import Icon from "./icons";
@ -43,6 +44,12 @@ const navigation: INavItem[] = [
icon: GalleryHorizontalEnd,
breadcrumbs: [{ name: "Gallery", href: "/gallery", current: true }],
},
{
name: "Deploy",
href: "/deploy",
icon: Rocket,
breadcrumbs: [{ name: "Deploy", href: "/deploy", current: true }],
},
];
const classNames = (...classes: (string | undefined | boolean)[]) => {

View File

@ -0,0 +1,66 @@
import React from "react";
import { Alert } from "antd";
import { CodeSection, copyToClipboard } from "./guides";
const DockerGuide: React.FC = () => {
return (
<div className="max-w-4xl">
<h1 className="tdext-2xl font-bold mb-6">Docker Container Setup</h1>
<Alert
className="mb-6"
message="Prerequisites"
description={
<ul className="list-disc pl-4 mt-2 space-y-1">
<li>Docker installed on your system</li>
</ul>
}
type="info"
/>
<CodeSection
title="1. Dockerfile"
description=<div>
AutoGen Studio provides a
<a
href="https://github.com/microsoft/autogen/blob/main/python/packages/autogen-studio/Dockerfile"
target="_blank"
rel="noreferrer"
className="text-accent underline px-1"
>
Dockerfile
</a>
that you can use to build your Docker container.{" "}
</div>
code={`FROM mcr.microsoft.com/devcontainers/python:3.10
WORKDIR /code
RUN pip install -U gunicorn autogenstudio
RUN useradd -m -u 1000 user
USER user
ENV HOME=/home/user
PATH=/home/user/.local/bin:$PATH
AUTOGENSTUDIO_APPDIR=/home/user/app
WORKDIR $HOME/app
COPY --chown=user . $HOME/app
CMD gunicorn -w $((2 * $(getconf _NPROCESSORS_ONLN) + 1)) --timeout 12600 -k uvicorn.workers.UvicornWorker autogenstudio.web.app:app --bind "0.0.0.0:8081"`}
onCopy={copyToClipboard}
/>
{/* Build and Run */}
<CodeSection
title="2. Build and Run"
description="Build and run your Docker container:"
code={`docker build -t autogenstudio .
docker run -p 8000:8000 autogenstudio`}
onCopy={copyToClipboard}
/>
</div>
);
};
export default DockerGuide;

View File

@ -0,0 +1,78 @@
import React from "react";
import { Copy } from "lucide-react";
import { Guide } from "../types";
import PythonGuide from "./python";
import DockerGuide from "./docker";
import { PrismLight as SyntaxHighlighter } from "react-syntax-highlighter";
import js from "react-syntax-highlighter/dist/esm/languages/prism/javascript";
import python from "react-syntax-highlighter/dist/esm/languages/prism/python";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
SyntaxHighlighter.registerLanguage("javascript", js);
SyntaxHighlighter.registerLanguage("python", python);
interface GuideContentProps {
guide: Guide;
}
export const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
export const GuideContent: React.FC<GuideContentProps> = ({ guide }) => {
// Render different content based on guide type and id
switch (guide.id) {
case "python-setup":
return <PythonGuide />;
case "docker-setup":
return <DockerGuide />;
// Add more cases for other guides...
default:
return (
<div className="text-secondary">
A Guide with the title <strong>{guide.title}</strong> is work in
progress!
</div>
);
}
};
interface CodeSectionProps {
title: string;
description?: string | React.ReactNode;
code?: string;
onCopy: (text: string) => void;
language?: string;
}
export const CodeSection: React.FC<CodeSectionProps> = ({
title,
description,
code,
onCopy,
language = "python",
}) => (
<section className="mt-6 bg-seco">
<h2 className="text-md font-semibold mb-3">{title}</h2>
{description && <p className=" mb-3">{description}</p>}
{code && (
<div className="relative bg-secondary text-sm p-4 rounded overflow-auto scroll">
<button
onClick={() => onCopy(code)}
className="absolute right-2 top-2 p-2 bg-secondary hover:bg-primary rounded-md"
>
<Copy className="w-4 h-4 hover:text-accent transition duration-100" />
</button>
{/* overflow scroll custom style */}
<SyntaxHighlighter language={language} wrapLines={true} style={oneDark}>
{code}
</SyntaxHighlighter>
</div>
)}
</section>
);
export default GuideContent;

View File

@ -0,0 +1,68 @@
import React from "react";
import { Alert } from "antd";
import { CodeSection, copyToClipboard } from "./guides";
import { Download } from "lucide-react";
const PythonGuide: React.FC = () => {
return (
<div className="max-w-4xl">
<h1 className="tdext-2xl font-bold mb-6">
Using AutoGen Studio Teams in Python Code and REST API
</h1>
<Alert
className="mb-6"
message="Prerequisites"
description={
<ul className="list-disc pl-4 mt-2 space-y-1">
<li>AutoGen Studio installed</li>
</ul>
}
type="info"
/>
<div className="my-3 text-sm">
{" "}
You can reuse the declarative specifications of agent teams created in
AutoGen studio in your python application by using the TeamManager
class. . In TeamBuilder, select a team configuration and click download.{" "}
<Download className="h-4 w-4 inline-block" />{" "}
</div>
{/* Installation Steps */}
<div className="space-y-6">
{/* Basic Usage */}
<CodeSection
title="1. Run a Team in Python"
description="Here's a simple example of using the TeamManager class from AutoGen Studio in your python code."
code={`from autogenstudio.teammanager import TeamManager
# Initialize the TeamManager
manager = TeamManager()
# Run a task with a specific team configuration
result = await manager.run(
task="What is the weather in New York?",
team_config="team.json"
)
print(result)`}
onCopy={copyToClipboard}
/>
<CodeSection
title="2. Serve a Team as a REST API"
description=<div>
AutoGen Studio offers a convenience CLI command to serve a team as a
REST API endpoint.{" "}
</div>
code={`
autogenstudio serve --team path/to/team.json --port 8084
`}
onCopy={copyToClipboard}
/>
</div>
</div>
);
};
export default PythonGuide;

View File

@ -0,0 +1,86 @@
import React, { useState, useEffect } from "react";
import { ChevronRight, TriangleAlert } from "lucide-react";
import { DeploySidebar } from "./sidebar";
import { Guide, defaultGuides } from "./types";
import { GuideContent } from "./guides/guides";
export const DeployManager: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [guides, setGuides] = useState<Guide[]>(defaultGuides);
const [currentGuide, setCurrentGuide] = useState<Guide | null>(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(() => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem("deploySidebar");
return stored !== null ? JSON.parse(stored) : true;
}
return true;
});
// Persist sidebar state
useEffect(() => {
if (typeof window !== "undefined") {
localStorage.setItem("deploySidebar", JSON.stringify(isSidebarOpen));
}
}, [isSidebarOpen]);
// Set first guide as current if none selected
useEffect(() => {
if (!currentGuide && guides.length > 0) {
setCurrentGuide(guides[0]);
}
}, [guides, currentGuide]);
return (
<div className="relative flex h-full w-full">
{/* Sidebar */}
<div
className={`absolute left-0 top-0 h-full transition-all duration-200 ease-in-out ${
isSidebarOpen ? "w-64" : "w-12"
}`}
>
<DeploySidebar
isOpen={isSidebarOpen}
guides={guides}
currentGuide={currentGuide}
onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
onSelectGuide={setCurrentGuide}
isLoading={isLoading}
/>
</div>
{/* Main Content */}
<div
className={`flex-1 transition-all -mr-6 duration-200 ${
isSidebarOpen ? "ml-64" : "ml-12"
}`}
>
<div className="p-4 pt-2">
{/* Breadcrumb */}
<div className="flex items-center gap-2 mb-4 text-sm">
<span className="text-primary font-medium">Deploy</span>
{currentGuide && (
<>
<ChevronRight className="w-4 h-4 text-secondary" />
<span className="text-secondary">{currentGuide.title}</span>
</>
)}
</div>
<div className="rounded border border-secondary border-dashed p-2 text-sm mb-4">
<TriangleAlert className="w-4 h-4 inline-block mr-2 -mt-1 text-secondary " />{" "}
The deployment guide section is work in progress.
</div>
{/* Content Area */}
{currentGuide ? (
<GuideContent guide={currentGuide} />
) : (
<div className="flex items-center justify-center h-[calc(100vh-190px)] text-secondary">
Select a guide from the sidebar to get started
</div>
)}
</div>
</div>
</div>
);
};
export default DeployManager;

View File

@ -0,0 +1,111 @@
import React from "react";
import { Button, Tooltip } from "antd";
import {
PanelLeftClose,
PanelLeftOpen,
Book,
InfoIcon,
RefreshCcw,
} from "lucide-react";
import type { Guide } from "./types";
interface DeploySidebarProps {
isOpen: boolean;
guides: Guide[];
currentGuide: Guide | null;
onToggle: () => void;
onSelectGuide: (guide: Guide) => void;
isLoading?: boolean;
}
export const DeploySidebar: React.FC<DeploySidebarProps> = ({
isOpen,
guides,
currentGuide,
onToggle,
onSelectGuide,
isLoading = false,
}) => {
// Render collapsed state
if (!isOpen) {
return (
<div className="h-full border-r border-secondary">
<div className="p-2 -ml-2">
<Tooltip title="Documentation">
<button
onClick={onToggle}
className="p-2 rounded-md hover:bg-secondary hover:text-accent text-secondary transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50"
>
<PanelLeftOpen strokeWidth={1.5} className="h-6 w-6" />
</button>
</Tooltip>
</div>
</div>
);
}
return (
<div className="h-full border-r border-secondary">
{/* Header */}
<div className="flex items-center justify-between pt-0 p-4 pl-2 pr-2 border-b border-secondary">
<div className="flex items-center gap-2">
{/* <Book className="w-4 h-4" /> */}
<span className="text-primary font-medium">Guides</span>
{/* <span className="px-2 py-0.5 text-xs bg-accent/10 text-accent rounded">
{guides.length}
</span> */}
</div>
<Tooltip title="Close Sidebar">
<button
onClick={onToggle}
className="p-2 rounded-md hover:bg-secondary hover:text-accent text-secondary transition-colors focus:outline-none focus:ring-2 focus:ring-accent focus:ring-opacity-50"
>
<PanelLeftClose strokeWidth={1.5} className="h-6 w-6" />
</button>
</Tooltip>
</div>
{/* Loading State */}
{isLoading && (
<div className="p-4">
<RefreshCcw className="w-4 h-4 inline-block animate-spin" />
</div>
)}
{/* Empty State */}
{!isLoading && guides.length === 0 && (
<div className="p-2 m-2 text-center text-secondary text-sm border border-dashed rounded">
<InfoIcon className="w-4 h-4 inline-block mr-1.5 -mt-0.5" />
No deployment guide available
</div>
)}
{/* Guides List */}
<div className="overflow-y-auto h-[calc(100%-64px)] mt-4">
{guides.map((guide) => (
<div key={guide.id} className="relative">
<div
className={`absolute top-1 left-0.5 z-50 h-[calc(100%-8px)]
w-1 bg-opacity-80 rounded ${
currentGuide?.id === guide.id ? "bg-accent" : "bg-tertiary"
}`}
/>
<div
className={`group ml-1 flex flex-col p-2 rounded-l cursor-pointer hover:bg-secondary ${
currentGuide?.id === guide.id
? "border-accent bg-secondary"
: "border-transparent"
}`}
onClick={() => onSelectGuide(guide)}
>
{/* Guide Title */}
<div className="flex items-center justify-between">
<span className="text-sm truncate">{guide.title}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,23 @@
export interface Guide {
id: string;
title: string;
type: "python" | "docker" | "cloud";
}
export const defaultGuides: Guide[] = [
{
id: "python-setup",
title: "Python",
type: "python",
},
{
id: "docker-setup",
title: "Docker",
type: "docker",
},
// {
// id: "cloud-deploy",
// title: "Cloud",
// type: "cloud",
// },
];

View File

@ -292,7 +292,7 @@ export const TeamBuilder: React.FC<TeamBuilderProps> = ({
</span>
</div>
<div>
<Tooltip title="Download Team Configuration">
<Tooltip title="Download Team">
<Button
type="text"
icon={<Download size={18} />}

View File

@ -116,11 +116,9 @@ export const TeamSidebar: React.FC<TeamSidebarProps> = ({
</div>
{/* Section Label */}
<div className="py-2 text-sm text-secondary">
Recents
{isLoading && (
<RefreshCcw className="w-4 h-4 inline-block ml-2 animate-spin" />
)}
<div className="py-2 flex text-sm text-secondary">
<div className="flex"> Recents</div>
{isLoading && <RefreshCcw className="w-4 h-4 ml-2 animate-spin" />}
</div>
{/* Teams List */}
@ -137,9 +135,7 @@ export const TeamSidebar: React.FC<TeamSidebarProps> = ({
{teams.length > 0 && (
<div
key={"teams_title"}
className={` ${
isLoading ? "opacity-50 pointer-events-none" : ""
}`}
className={` ${isLoading ? " pointer-events-none" : ""}`}
>
{" "}
{teams.map((team) => (

View File

@ -0,0 +1,28 @@
import * as React from "react";
import Layout from "../components/layout";
import { graphql } from "gatsby";
import DeployManager from "../components/views/deploy/manager";
// markup
const DeployPage = ({ data }: any) => {
return (
<Layout meta={data.site.siteMetadata} title="Home" link={"/deploy"}>
<main style={{ height: "100%" }} className=" h-full ">
<DeployManager />
</main>
</Layout>
);
};
export const query = graphql`
query HomePageQuery {
site {
siteMetadata {
description
title
}
}
}
`;
export default DeployPage;

File diff suppressed because it is too large Load Diff