feat(server): backend analytics endpoints (#8030)

This commit is contained in:
Nicholas Tindle
2024-09-18 13:23:20 -05:00
committed by GitHub
parent d8f989daf8
commit 86db4deef9
20 changed files with 586 additions and 310 deletions

View File

@@ -1,7 +1,7 @@
NEXT_PUBLIC_AUTH_CALLBACK_URL=http://localhost:8006/auth/callback
NEXT_PUBLIC_AGPT_SERVER_URL=http://localhost:8006/api
NEXT_PUBLIC_AGPT_WS_SERVER_URL=ws://localhost:8001/ws
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8005/api/v1/market
NEXT_PUBLIC_AGPT_MARKETPLACE_URL=http://localhost:8015/api/v1/market
## Supabase credentials

View File

@@ -35,6 +35,7 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
}
export async function signup(values: z.infer<typeof loginFormSchema>) {
"use server";
return await Sentry.withServerActionInstrumentation(
"signup",
{},

View File

@@ -7,7 +7,7 @@ import AgentDetailContent from "@/components/marketplace/AgentDetailContent";
async function getAgentDetails(id: string): Promise<AgentDetailResponse> {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8001/api/v1/market";
"http://localhost:8015/api/v1/market";
const api = new MarketplaceAPI(apiUrl);
try {
console.log(`Fetching agent details for id: ${id}`);

View File

@@ -185,7 +185,7 @@ const Pagination: React.FC<{
const Marketplace: React.FC = () => {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8001/api/v1/market";
"http://localhost:8015/api/v1/market";
const api = useMemo(() => new MarketplaceAPI(apiUrl), [apiUrl]);
const [searchValue, setSearchValue] = useState("");

View File

@@ -1,5 +1,4 @@
"use server";
import AutoGPTServerAPI from "@/lib/autogpt-server-api";
import MarketplaceAPI from "@/lib/marketplace-api";
import { revalidatePath } from "next/cache";
import * as Sentry from "@sentry/nextjs";

View File

@@ -81,7 +81,7 @@ function convertGraphToReactFlow(graph: any): { nodes: Node[]; edges: Edge[] } {
async function installGraph(id: string): Promise<void> {
const apiUrl =
process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8001/api/v1/market";
"http://localhost:8015/api/v1/market";
const api = new MarketplaceAPI(apiUrl);
const serverAPIUrl = process.env.AGPT_SERVER_API_URL;

View File

@@ -1,3 +1,4 @@
import { sendGAEvent } from "@next/third-parties/google";
import Shepherd from "shepherd.js";
import "shepherd.js/dist/css/shepherd.css";
@@ -493,6 +494,15 @@ export const startTutorial = (
localStorage.setItem("shepherd-tour", "completed"); // Optionally mark the tutorial as completed
});
for (const step of tour.steps) {
step.on("show", () => {
"use client";
console.debug("sendTutorialStep");
sendGAEvent("event", "tutorial_step_shown", { value: step.id });
});
}
tour.on("cancel", () => {
setPinBlocksPopover(false);
localStorage.setItem("shepherd-tour", "canceled"); // Optionally mark the tutorial as canceled

View File

@@ -0,0 +1,317 @@
import { SupabaseClient } from "@supabase/supabase-js";
import {
Block,
Graph,
GraphCreatable,
GraphUpdateable,
GraphMeta,
GraphExecuteResponse,
NodeExecutionResult,
User,
AnalyticsMetrics,
AnalyticsDetails,
} from "./types";
export default class BaseAutoGPTServerAPI {
private baseUrl: string;
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
private supabaseClient: SupabaseClient | null = null;
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8000/api",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
supabaseClient: SupabaseClient | null = null,
) {
this.baseUrl = baseUrl;
this.wsUrl = wsUrl;
this.supabaseClient = supabaseClient;
}
async createUser(): Promise<User> {
return this._request("POST", "/auth/user", {});
}
async getBlocks(): Promise<Block[]> {
return await this._get("/blocks");
}
async listGraphs(): Promise<GraphMeta[]> {
return this._get("/graphs");
}
async listTemplates(): Promise<GraphMeta[]> {
return this._get("/templates");
}
async getGraph(id: string, version?: number): Promise<Graph> {
const query = version !== undefined ? `?version=${version}` : "";
return this._get(`/graphs/${id}` + query);
}
async getTemplate(id: string, version?: number): Promise<Graph> {
const query = version !== undefined ? `?version=${version}` : "";
return this._get(`/templates/${id}` + query);
}
async getGraphAllVersions(id: string): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
async getTemplateAllVersions(id: string): Promise<Graph[]> {
return this._get(`/templates/${id}/versions`);
}
async createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
async createGraph(
fromTemplateID: string,
templateVersion: number,
): Promise<Graph>;
async createGraph(
graphOrTemplateID: GraphCreatable | string,
templateVersion?: number,
): Promise<Graph> {
let requestBody: GraphCreateRequestBody;
if (typeof graphOrTemplateID == "string") {
if (templateVersion == undefined) {
throw new Error("templateVersion not specified");
}
requestBody = {
template_id: graphOrTemplateID,
template_version: templateVersion,
};
} else {
requestBody = { graph: graphOrTemplateID };
}
return this._request("POST", "/graphs", requestBody);
}
async createTemplate(templateCreateBody: GraphCreatable): Promise<Graph> {
const requestBody: GraphCreateRequestBody = { graph: templateCreateBody };
return this._request("POST", "/templates", requestBody);
}
async updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
return await this._request("PUT", `/graphs/${id}`, graph);
}
async updateTemplate(id: string, template: GraphUpdateable): Promise<Graph> {
return await this._request("PUT", `/templates/${id}`, template);
}
async setGraphActiveVersion(id: string, version: number): Promise<Graph> {
return this._request("PUT", `/graphs/${id}/versions/active`, {
active_graph_version: version,
});
}
async executeGraph(
id: string,
inputData: { [key: string]: any } = {},
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
}
async listGraphRunIDs(
graphID: string,
graphVersion?: number,
): Promise<string[]> {
const query =
graphVersion !== undefined ? `?graph_version=${graphVersion}` : "";
return this._get(`/graphs/${graphID}/executions` + query);
}
async getGraphExecutionInfo(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
parseNodeExecutionResultTimestamps,
);
}
async stopGraphExecution(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
).map(parseNodeExecutionResultTimestamps);
}
async logMetric(metric: AnalyticsMetrics) {
return this._request("POST", "/analytics/log_raw_metric", metric);
}
async logAnalytic(analytic: AnalyticsDetails) {
return this._request("POST", "/analytics/log_raw_analytics", analytic);
}
private async _get(path: string) {
return this._request("GET", path);
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH",
path: string,
payload?: { [key: string]: any },
) {
if (method != "GET") {
console.debug(`${method} ${path} payload:`, payload);
}
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const response = await fetch(this.baseUrl + path, {
method,
headers:
method != "GET"
? {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
}
: {
Authorization: token ? `Bearer ${token}` : "",
},
body: JSON.stringify(payload),
});
const response_data = await response.json();
if (!response.ok) {
console.warn(
`${method} ${path} returned non-OK response:`,
response_data.detail,
response,
);
throw new Error(`HTTP error ${response.status}! ${response_data.detail}`);
}
return response_data;
}
async connectWebSocket(): Promise<void> {
this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.debug("WebSocket connection established");
resolve();
};
this.webSocket.onclose = (event) => {
console.debug("WebSocket connection closed", event);
this.webSocket = null;
};
this.webSocket.onerror = (error) => {
console.error("WebSocket error:", error);
reject(error);
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
if (message.method == "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
} catch (error) {
console.error("Error connecting to WebSocket:", error);
reject(error);
}
});
return this.wsConnecting;
}
disconnectWebSocket() {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.close();
}
}
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
data: WebsocketMessageTypeMap[M],
callCount = 0,
) {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(JSON.stringify({ method, data }));
} else {
this.connectWebSocket().then(() => {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
): () => void {
this.wsMessageHandlers[method] ??= new Set();
this.wsMessageHandlers[method].add(handler);
// Return detacher
return () => this.wsMessageHandlers[method].delete(handler);
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
}
}
/* *** UTILITY TYPES *** */
type GraphCreateRequestBody =
| {
template_id: string;
template_version: number;
}
| {
graph: GraphCreatable;
};
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
execution_event: NodeExecutionResult;
};
type WebsocketMessage = {
[M in keyof WebsocketMessageTypeMap]: {
method: M;
data: WebsocketMessageTypeMap[M];
};
}[keyof WebsocketMessageTypeMap];
/* *** HELPER FUNCTIONS *** */
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
return {
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
};
}

View File

@@ -1,305 +1,14 @@
import { createClient } from "../supabase/client";
import {
Block,
Graph,
GraphCreatable,
GraphUpdateable,
GraphMeta,
GraphExecuteResponse,
NodeExecutionResult,
User,
} from "./types";
export default class AutoGPTServerAPI {
private baseUrl: string;
private wsUrl: string;
private webSocket: WebSocket | null = null;
private wsConnecting: Promise<void> | null = null;
private wsMessageHandlers: Record<string, Set<(data: any) => void>> = {};
private supabaseClient = createClient();
import BaseAutoGPTServerAPI from "./baseClient";
export default class AutoGPTServerAPI extends BaseAutoGPTServerAPI {
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8000/api",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
) {
this.baseUrl = baseUrl;
this.wsUrl = wsUrl;
}
async createUser(): Promise<User> {
return this._request("POST", "/auth/user", {});
}
async getBlocks(): Promise<Block[]> {
return await this._get("/blocks");
}
async listGraphs(): Promise<GraphMeta[]> {
return this._get("/graphs");
}
async listTemplates(): Promise<GraphMeta[]> {
return this._get("/templates");
}
async getGraph(id: string, version?: number): Promise<Graph> {
const query = version !== undefined ? `?version=${version}` : "";
return this._get(`/graphs/${id}` + query);
}
async getTemplate(id: string, version?: number): Promise<Graph> {
const query = version !== undefined ? `?version=${version}` : "";
return this._get(`/templates/${id}` + query);
}
async getGraphAllVersions(id: string): Promise<Graph[]> {
return this._get(`/graphs/${id}/versions`);
}
async getTemplateAllVersions(id: string): Promise<Graph[]> {
return this._get(`/templates/${id}/versions`);
}
async createGraph(graphCreateBody: GraphCreatable): Promise<Graph>;
async createGraph(
fromTemplateID: string,
templateVersion: number,
): Promise<Graph>;
async createGraph(
graphOrTemplateID: GraphCreatable | string,
templateVersion?: number,
): Promise<Graph> {
let requestBody: GraphCreateRequestBody;
if (typeof graphOrTemplateID == "string") {
if (templateVersion == undefined) {
throw new Error("templateVersion not specified");
}
requestBody = {
template_id: graphOrTemplateID,
template_version: templateVersion,
};
} else {
requestBody = { graph: graphOrTemplateID };
}
return this._request("POST", "/graphs", requestBody);
}
async createTemplate(templateCreateBody: GraphCreatable): Promise<Graph> {
const requestBody: GraphCreateRequestBody = { graph: templateCreateBody };
return this._request("POST", "/templates", requestBody);
}
async updateGraph(id: string, graph: GraphUpdateable): Promise<Graph> {
return await this._request("PUT", `/graphs/${id}`, graph);
}
async updateTemplate(id: string, template: GraphUpdateable): Promise<Graph> {
return await this._request("PUT", `/templates/${id}`, template);
}
async setGraphActiveVersion(id: string, version: number): Promise<Graph> {
return this._request("PUT", `/graphs/${id}/versions/active`, {
active_graph_version: version,
});
}
async executeGraph(
id: string,
inputData: { [key: string]: any } = {},
): Promise<GraphExecuteResponse> {
return this._request("POST", `/graphs/${id}/execute`, inputData);
}
async listGraphRunIDs(
graphID: string,
graphVersion?: number,
): Promise<string[]> {
const query =
graphVersion !== undefined ? `?graph_version=${graphVersion}` : "";
return this._get(`/graphs/${graphID}/executions` + query);
}
async getGraphExecutionInfo(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (await this._get(`/graphs/${graphID}/executions/${runID}`)).map(
parseNodeExecutionResultTimestamps,
);
}
async stopGraphExecution(
graphID: string,
runID: string,
): Promise<NodeExecutionResult[]> {
return (
await this._request("POST", `/graphs/${graphID}/executions/${runID}/stop`)
).map(parseNodeExecutionResultTimestamps);
}
private async _get(path: string) {
return this._request("GET", path);
}
private async _request(
method: "GET" | "POST" | "PUT" | "PATCH",
path: string,
payload?: { [key: string]: any },
) {
if (method != "GET") {
console.debug(`${method} ${path} payload:`, payload);
}
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const response = await fetch(this.baseUrl + path, {
method,
headers:
method != "GET"
? {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
}
: {
Authorization: token ? `Bearer ${token}` : "",
},
body: JSON.stringify(payload),
});
const response_data = await response.json();
if (!response.ok) {
console.warn(
`${method} ${path} returned non-OK response:`,
response_data.detail,
response,
);
throw new Error(`HTTP error ${response.status}! ${response_data.detail}`);
}
return response_data;
}
async connectWebSocket(): Promise<void> {
this.wsConnecting ??= new Promise(async (resolve, reject) => {
try {
const token =
(await this.supabaseClient?.auth.getSession())?.data.session
?.access_token || "";
const wsUrlWithToken = `${this.wsUrl}?token=${token}`;
this.webSocket = new WebSocket(wsUrlWithToken);
this.webSocket.onopen = () => {
console.debug("WebSocket connection established");
resolve();
};
this.webSocket.onclose = (event) => {
console.debug("WebSocket connection closed", event);
this.webSocket = null;
};
this.webSocket.onerror = (error) => {
console.error("WebSocket error:", error);
reject(error);
};
this.webSocket.onmessage = (event) => {
const message: WebsocketMessage = JSON.parse(event.data);
if (message.method == "execution_event") {
message.data = parseNodeExecutionResultTimestamps(message.data);
}
this.wsMessageHandlers[message.method]?.forEach((handler) =>
handler(message.data),
);
};
} catch (error) {
console.error("Error connecting to WebSocket:", error);
reject(error);
}
});
return this.wsConnecting;
}
disconnectWebSocket() {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.close();
}
}
sendWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
data: WebsocketMessageTypeMap[M],
callCount = 0,
) {
if (this.webSocket && this.webSocket.readyState === WebSocket.OPEN) {
this.webSocket.send(JSON.stringify({ method, data }));
} else {
this.connectWebSocket().then(() => {
callCount == 0
? this.sendWebSocketMessage(method, data, callCount + 1)
: setTimeout(
() => {
this.sendWebSocketMessage(method, data, callCount + 1);
},
2 ** (callCount - 1) * 1000,
);
});
}
}
onWebSocketMessage<M extends keyof WebsocketMessageTypeMap>(
method: M,
handler: (data: WebsocketMessageTypeMap[M]) => void,
): () => void {
this.wsMessageHandlers[method] ??= new Set();
this.wsMessageHandlers[method].add(handler);
// Return detacher
return () => this.wsMessageHandlers[method].delete(handler);
}
subscribeToExecution(graphId: string) {
this.sendWebSocketMessage("subscribe", { graph_id: graphId });
const supabaseClient = createClient();
super(baseUrl, wsUrl, supabaseClient);
}
}
/* *** UTILITY TYPES *** */
type GraphCreateRequestBody =
| {
template_id: string;
template_version: number;
}
| {
graph: GraphCreatable;
};
type WebsocketMessageTypeMap = {
subscribe: { graph_id: string };
execution_event: NodeExecutionResult;
};
type WebsocketMessage = {
[M in keyof WebsocketMessageTypeMap]: {
method: M;
data: WebsocketMessageTypeMap[M];
};
}[keyof WebsocketMessageTypeMap];
/* *** HELPER FUNCTIONS *** */
function parseNodeExecutionResultTimestamps(result: any): NodeExecutionResult {
return {
...result,
add_time: new Date(result.add_time),
queue_time: result.queue_time ? new Date(result.queue_time) : undefined,
start_time: result.start_time ? new Date(result.start_time) : undefined,
end_time: result.end_time ? new Date(result.end_time) : undefined,
};
}

View File

@@ -0,0 +1,14 @@
import { createServerClient } from "../supabase/server";
import BaseAutoGPTServerAPI from "./baseClient";
export default class AutoGPTServerAPIServerSide extends BaseAutoGPTServerAPI {
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_SERVER_URL ||
"http://localhost:8000/api",
wsUrl: string = process.env.NEXT_PUBLIC_AGPT_WS_SERVER_URL ||
"ws://localhost:8001/ws",
) {
const supabaseClient = createServerClient();
super(baseUrl, wsUrl, supabaseClient);
}
}

View File

@@ -190,3 +190,15 @@ export enum BlockUIType {
OUTPUT = "Output",
NOTE = "Note",
}
export type AnalyticsMetrics = {
metric_name: string;
metric_value: number;
data_string: string;
};
export type AnalyticsDetails = {
type: string;
data: { [key: string]: any };
index: string;
};

View File

@@ -17,7 +17,7 @@ export default class MarketplaceAPI {
constructor(
baseUrl: string = process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL ||
"http://localhost:8001/api/v1/market",
"http://localhost:8015/api/v1/market",
) {
this.baseUrl = baseUrl;
}

View File

@@ -1,3 +1,6 @@
{
"python.analysis.typeCheckingMode": "basic",
"python.testing.pytestArgs": ["test"],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View File

@@ -0,0 +1,43 @@
import logging
import prisma.types
logger = logging.getLogger(__name__)
async def log_raw_analytics(
user_id: str,
type: str,
data: dict,
data_index: str,
):
details = await prisma.models.AnalyticsDetails.prisma().create(
data={
"userId": user_id,
"type": type,
"data": prisma.Json(data),
"dataIndex": data_index,
}
)
return details
async def log_raw_metric(
user_id: str,
metric_name: str,
metric_value: float,
data_string: str,
):
if metric_value < 0:
raise ValueError("metric_value must be non-negative")
result = await prisma.models.AnalyticsMetrics.prisma().create(
data={
"value": metric_value,
"analyticMetric": metric_name,
"userId": user_id,
"dataString": data_string,
},
)
return result

View File

@@ -78,20 +78,35 @@ class AgentServer(AppService):
api_router.dependencies.append(Depends(auth_middleware))
# Import & Attach sub-routers
from .integrations import integrations_api_router
import autogpt_server.server.routers.analytics
import autogpt_server.server.routers.integrations
api_router.include_router(integrations_api_router, prefix="/integrations")
api_router.include_router(
autogpt_server.server.routers.integrations.router,
prefix="/integrations",
tags=["integrations"],
dependencies=[Depends(auth_middleware)],
)
api_router.include_router(
autogpt_server.server.routers.analytics.router,
prefix="/analytics",
tags=["analytics"],
dependencies=[Depends(auth_middleware)],
)
api_router.add_api_route(
path="/auth/user",
endpoint=self.get_or_create_user_route,
methods=["POST"],
tags=["auth"],
)
api_router.add_api_route(
path="/blocks",
endpoint=self.get_graph_blocks,
methods=["GET"],
tags=["blocks"],
)
api_router.add_api_route(
path="/blocks/costs",
@@ -102,106 +117,127 @@ class AgentServer(AppService):
path="/blocks/{block_id}/execute",
endpoint=self.execute_graph_block,
methods=["POST"],
tags=["blocks"],
)
api_router.add_api_route(
path="/graphs",
endpoint=self.get_graphs,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/templates",
endpoint=self.get_templates,
methods=["GET"],
tags=["templates", "graphs"],
)
api_router.add_api_route(
path="/graphs",
endpoint=self.create_new_graph,
methods=["POST"],
tags=["graphs"],
)
api_router.add_api_route(
path="/templates",
endpoint=self.create_new_template,
methods=["POST"],
tags=["templates", "graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}",
endpoint=self.get_graph,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/templates/{graph_id}",
endpoint=self.get_template,
methods=["GET"],
tags=["templates", "graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}",
endpoint=self.update_graph,
methods=["PUT"],
tags=["graphs"],
)
api_router.add_api_route(
path="/templates/{graph_id}",
endpoint=self.update_graph,
methods=["PUT"],
tags=["templates", "graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/versions",
endpoint=self.get_graph_all_versions,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/templates/{graph_id}/versions",
endpoint=self.get_graph_all_versions,
methods=["GET"],
tags=["templates", "graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/versions/{version}",
endpoint=self.get_graph,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/versions/active",
endpoint=self.set_graph_active_version,
methods=["PUT"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/input_schema",
endpoint=self.get_graph_input_schema,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/execute",
endpoint=self.execute_graph,
methods=["POST"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/executions",
endpoint=self.list_graph_runs,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/executions/{graph_exec_id}",
endpoint=self.get_graph_run_node_execution_results,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/executions/{graph_exec_id}/stop",
endpoint=self.stop_graph_run,
methods=["POST"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/schedules",
endpoint=self.create_schedule,
methods=["POST"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/{graph_id}/schedules",
endpoint=self.get_execution_schedules,
methods=["GET"],
tags=["graphs"],
)
api_router.add_api_route(
path="/graphs/schedules/{schedule_id}",
endpoint=self.update_schedule,
methods=["PUT"],
tags=["graphs"],
)
api_router.add_api_route(
path="/credits",
@@ -213,6 +249,7 @@ class AgentServer(AppService):
path="/settings",
endpoint=self.update_configuration,
methods=["POST"],
tags=["settings"],
)
app.add_exception_handler(500, self.handle_internal_http_error)

View File

@@ -0,0 +1,49 @@
"""Analytics API"""
from typing import Annotated
import fastapi
import autogpt_server.data.analytics
from autogpt_server.server.utils import get_user_id
router = fastapi.APIRouter()
@router.post(path="/log_raw_metric")
async def log_raw_metric(
user_id: Annotated[str, fastapi.Depends(get_user_id)],
metric_name: Annotated[str, fastapi.Body(..., embed=True)],
metric_value: Annotated[float, fastapi.Body(..., embed=True)],
data_string: Annotated[str, fastapi.Body(..., embed=True)],
):
result = await autogpt_server.data.analytics.log_raw_metric(
user_id=user_id,
metric_name=metric_name,
metric_value=metric_value,
data_string=data_string,
)
return result.id
@router.post("/log_raw_analytics")
async def log_raw_analytics(
user_id: Annotated[str, fastapi.Depends(get_user_id)],
type: Annotated[str, fastapi.Body(..., embed=True)],
data: Annotated[
dict,
fastapi.Body(..., embed=True, description="The data to log"),
],
data_index: Annotated[
str,
fastapi.Body(
...,
embed=True,
description="Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc.",
),
],
):
result = await autogpt_server.data.analytics.log_raw_analytics(
user_id, type, data, data_index
)
return result.id

View File

@@ -15,11 +15,11 @@ from supabase import Client
from autogpt_server.integrations.oauth import HANDLERS_BY_NAME, BaseOAuthHandler
from autogpt_server.util.settings import Settings
from .utils import get_supabase, get_user_id
from ..utils import get_supabase, get_user_id
logger = logging.getLogger(__name__)
settings = Settings()
integrations_api_router = APIRouter()
router = APIRouter()
def get_store(supabase: Client = Depends(get_supabase)):
@@ -30,7 +30,7 @@ class LoginResponse(BaseModel):
login_url: str
@integrations_api_router.get("/{provider}/login")
@router.get("/{provider}/login")
async def login(
provider: Annotated[str, Path(title="The provider to initiate an OAuth flow for")],
user_id: Annotated[str, Depends(get_user_id)],
@@ -59,7 +59,7 @@ class CredentialsMetaResponse(BaseModel):
username: str | None
@integrations_api_router.post("/{provider}/callback")
@router.post("/{provider}/callback")
async def callback(
provider: Annotated[str, Path(title="The target provider for this OAuth exchange")],
code: Annotated[str, Body(title="Authorization code acquired by user login")],
@@ -91,7 +91,7 @@ async def callback(
)
@integrations_api_router.get("/{provider}/credentials")
@router.get("/{provider}/credentials")
async def list_credentials(
provider: Annotated[str, Path(title="The provider to list credentials for")],
user_id: Annotated[str, Depends(get_user_id)],
@@ -110,7 +110,7 @@ async def list_credentials(
]
@integrations_api_router.get("/{provider}/credentials/{cred_id}")
@router.get("/{provider}/credentials/{cred_id}")
async def get_credential(
provider: Annotated[str, Path(title="The provider to retrieve credentials for")],
cred_id: Annotated[str, Path(title="The ID of the credentials to retrieve")],

View File

@@ -0,0 +1,37 @@
-- CreateTable
CREATE TABLE "AnalyticsDetails" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"data" JSONB,
"dataIndex" TEXT,
CONSTRAINT "AnalyticsDetails_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AnalyticsMetrics" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"analyticMetric" TEXT NOT NULL,
"value" DOUBLE PRECISION NOT NULL,
"dataString" TEXT,
"userId" TEXT NOT NULL,
CONSTRAINT "AnalyticsMetrics_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "analyticsDetails" ON "AnalyticsDetails"("userId", "type");
-- CreateIndex
CREATE INDEX "AnalyticsDetails_type_idx" ON "AnalyticsDetails"("type");
-- AddForeignKey
ALTER TABLE "AnalyticsDetails" ADD CONSTRAINT "AnalyticsDetails_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AnalyticsMetrics" ADD CONSTRAINT "AnalyticsMetrics_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -22,6 +22,8 @@ model User {
AgentGraphs AgentGraph[]
AgentGraphExecutions AgentGraphExecution[]
AgentGraphExecutionSchedules AgentGraphExecutionSchedule[]
AnalyticsDetails AnalyticsDetails[]
AnalyticsMetrics AnalyticsMetrics[]
UserBlockCredit UserBlockCredit[]
@@index([id])
@@ -212,6 +214,49 @@ model AgentGraphExecutionSchedule {
@@index([isEnabled])
}
model AnalyticsDetails {
// PK uses gen_random_uuid() to allow the db inserts to happen outside of prisma
// typical uuid() inserts are handled by prisma
id String @id @default(dbgenerated("gen_random_uuid()"))
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
// Link to User model
userId String
user User @relation(fields: [userId], references: [id])
// Analytics Categorical data used for filtering (indexable w and w/o userId)
type String
// Analytic Specific Data. We should use a union type here, but prisma doesn't support it.
data Json?
// Indexable field for any count based analytical measures like page order clicking, tutorial step completion, etc.
dataIndex String?
@@index([userId, type], name: "analyticsDetails")
@@index([type])
}
model AnalyticsMetrics {
id String @id @default(dbgenerated("gen_random_uuid()"))
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Analytics Categorical data used for filtering (indexable w and w/o userId)
analyticMetric String
// Any numeric data that should be counted upon, summed, or otherwise aggregated.
value Float
// Any string data that should be used to identify the metric as distinct.
// ex: '/build' vs '/market'
dataString String?
// Link to User model
userId String
user User @relation(fields: [userId], references: [id])
}
enum UserBlockCreditType {
TOP_UP
USAGE

View File

@@ -45,7 +45,7 @@ def populate_database():
keywords=["test"],
)
response = requests.post(
"http://localhost:8001/api/v1/market/admin/agent", json=req.model_dump()
"http://localhost:8015/api/v1/market/admin/agent", json=req.model_dump()
)
print(response.text)