From 2bcf373f0febe46e24b084821623efbdad9195cf Mon Sep 17 00:00:00 2001 From: Nicholas Tindle Date: Mon, 9 Sep 2024 19:55:29 -0500 Subject: [PATCH] feat(server): first backend endpoint --- rnd/autogpt_server/.vscode/settings.json | 3 + .../autogpt_server/server/rest_api.py | 32 +++++++ .../server/routers/analytics.py | 61 ++++++++++++ .../migration.sql | 53 +++++++++++ .../migration.sql | 2 + rnd/autogpt_server/schema.prisma | 92 +++++++++++++++++-- 6 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 rnd/autogpt_server/autogpt_server/server/routers/analytics.py create mode 100644 rnd/autogpt_server/migrations/20240910000943_add_analytics_to_backend/migration.sql create mode 100644 rnd/autogpt_server/migrations/20240910001417_add_aggregate_counter_for_avg_etc_events/migration.sql diff --git a/rnd/autogpt_server/.vscode/settings.json b/rnd/autogpt_server/.vscode/settings.json index 31f7f02be4..6715315223 100644 --- a/rnd/autogpt_server/.vscode/settings.json +++ b/rnd/autogpt_server/.vscode/settings.json @@ -1,3 +1,6 @@ { "python.analysis.typeCheckingMode": "basic", + "python.testing.pytestArgs": ["test"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true } diff --git a/rnd/autogpt_server/autogpt_server/server/rest_api.py b/rnd/autogpt_server/autogpt_server/server/rest_api.py index 6ba2653ab7..f868aea29b 100644 --- a/rnd/autogpt_server/autogpt_server/server/rest_api.py +++ b/rnd/autogpt_server/autogpt_server/server/rest_api.py @@ -78,128 +78,160 @@ class AgentServer(AppService): from .integrations import integrations_api_router api_router.include_router(integrations_api_router, prefix="/integrations") + import autogpt_server.server.routers.analytics + + 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/{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="/settings", endpoint=self.update_configuration, methods=["POST"], + tags=["settings"], ) app.add_exception_handler(500, self.handle_internal_http_error) diff --git a/rnd/autogpt_server/autogpt_server/server/routers/analytics.py b/rnd/autogpt_server/autogpt_server/server/routers/analytics.py new file mode 100644 index 0000000000..e7d642515f --- /dev/null +++ b/rnd/autogpt_server/autogpt_server/server/routers/analytics.py @@ -0,0 +1,61 @@ +# Analytics API + +from typing import Annotated +import typing +import typing_extensions +import fastapi +import prisma +import prisma.enums +import pydantic +from autogpt_server.server.utils import get_user_id + +router = fastapi.APIRouter() + + +class UserData(pydantic.BaseModel): + user_id: str + email: str + name: str + username: str + + +class TutorialStepData(pydantic.BaseModel): + data: typing.Optional[dict] + + +@router.post(path="/log_new_user") +async def log_create_user( + user_id: Annotated[str, fastapi.Depends(get_user_id)], + user_data: Annotated[UserData, fastapi.Body(..., embed=True)], +): + """ + Log the user ID for analytics purposes. + """ + id = await prisma.models.AnalyticsDetails.prisma().create( + data={ + "userId": user_id, + "type": prisma.enums.AnalyticsType.CREATE_USER, + "data": prisma.Json(user_data.model_dump_json()), + } + ) + return id.id + + +@router.post(path="/log_tutorial_step") +async def log_tutorial_step( + user_id: Annotated[str, fastapi.Depends(get_user_id)], + step: Annotated[int, fastapi.Query(..., embed=True)], + data: Annotated[TutorialStepData, fastapi.Body(..., embed=True)], +): + """ + Log the tutorial step completed by the user for analytics purposes. + """ + id = await prisma.models.AnalyticsDetails.prisma().create( + data={ + "userId": user_id, + "type": prisma.enums.AnalyticsType.TUTORIAL_STEP, + "data": prisma.Json(data.model_dump_json()), + "dataIndex": step, + } + ) + return id.id diff --git a/rnd/autogpt_server/migrations/20240910000943_add_analytics_to_backend/migration.sql b/rnd/autogpt_server/migrations/20240910000943_add_analytics_to_backend/migration.sql new file mode 100644 index 0000000000..b5fac3c85b --- /dev/null +++ b/rnd/autogpt_server/migrations/20240910000943_add_analytics_to_backend/migration.sql @@ -0,0 +1,53 @@ +-- CreateEnum +CREATE TYPE "AnalyticsType" AS ENUM ('CREATE_USER', 'TUTORIAL_STEP', 'WEB_PAGE', 'AGENT_GRAPH_EXECUTION', 'AGENT_NODE_EXECUTION'); + +-- CreateEnum +CREATE TYPE "AnalyticsMetric" AS ENUM ('PAGE_VIEW', 'TUTORIAL_STEP_COMPLETION', 'AGENT_GRAPH_EXECUTION', 'AGENT_NODE_EXECUTION'); + +-- CreateEnum +CREATE TYPE "AggregationType" AS ENUM ('COUNT', 'SUM', 'AVG', 'MAX', 'MIN', 'NO_AGGREGATION'); + +-- 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" "AnalyticsType" NOT NULL, + "data" JSONB, + "dataIndex" INTEGER, + + 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" "AnalyticsMetric" NOT NULL, + "value" DOUBLE PRECISION NOT NULL, + "dataString" TEXT, + "aggregationType" "AggregationType" NOT NULL DEFAULT 'NO_AGGREGATION', + "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"); + +-- CreateIndex +CREATE INDEX "analytics_metric_index" ON "AnalyticsMetrics"("analyticMetric", "userId", "dataString", "aggregationType"); + +-- CreateIndex +CREATE UNIQUE INDEX "AnalyticsMetrics_analyticMetric_userId_dataString_aggregati_key" ON "AnalyticsMetrics"("analyticMetric", "userId", "dataString", "aggregationType"); + +-- 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; diff --git a/rnd/autogpt_server/migrations/20240910001417_add_aggregate_counter_for_avg_etc_events/migration.sql b/rnd/autogpt_server/migrations/20240910001417_add_aggregate_counter_for_avg_etc_events/migration.sql new file mode 100644 index 0000000000..ea43012001 --- /dev/null +++ b/rnd/autogpt_server/migrations/20240910001417_add_aggregate_counter_for_avg_etc_events/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "AnalyticsMetrics" ADD COLUMN "aggregationCounter" INTEGER DEFAULT 1; diff --git a/rnd/autogpt_server/schema.prisma b/rnd/autogpt_server/schema.prisma index 014ea43922..207d097aee 100644 --- a/rnd/autogpt_server/schema.prisma +++ b/rnd/autogpt_server/schema.prisma @@ -22,6 +22,8 @@ model User { AgentGraphs AgentGraph[] AgentGraphExecutions AgentGraphExecution[] AgentGraphExecutionSchedules AgentGraphExecutionSchedule[] + AnalyticsDetails AnalyticsDetails[] + AnalyticsMetrics AnalyticsMetrics[] @@index([id]) @@index([email]) @@ -29,9 +31,9 @@ model User { // This model describes the Agent Graph/Flow (Multi Agent System). model AgentGraph { - id String @default(uuid()) - version Int @default(1) - createdAt DateTime @default(now()) + id String @default(uuid()) + version Int @default(1) + createdAt DateTime @default(now()) updatedAt DateTime? @updatedAt name String? @@ -115,8 +117,8 @@ model AgentBlock { // This model describes the execution of an AgentGraph. model AgentGraphExecution { - id String @id @default(uuid()) - createdAt DateTime @default(now()) + id String @id @default(uuid()) + createdAt DateTime @default(now()) updatedAt DateTime? @updatedAt agentGraphId String @@ -178,8 +180,8 @@ model AgentNodeExecutionInputOutput { // This model describes the recurring execution schedule of an Agent. model AgentGraphExecutionSchedule { - id String @id - createdAt DateTime @default(now()) + id String @id + createdAt DateTime @default(now()) updatedAt DateTime? @updatedAt agentGraphId String @@ -199,3 +201,79 @@ model AgentGraphExecutionSchedule { @@index([isEnabled]) } + +enum AnalyticsType { + CREATE_USER + TUTORIAL_STEP + WEB_PAGE + + AGENT_GRAPH_EXECUTION + AGENT_NODE_EXECUTION +} + +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 AnalyticsType + + // Analytic Specific Data. We should use a union type here, but prisma doesn't support it. + data Json? + + // Indexable field for any count based analytically measures like page order clicking, tutorial step completion, etc. + dataIndex Int? + + @@index([userId, type], name: "analyticsDetails") + @@index([type]) +} + +enum AnalyticsMetric { + PAGE_VIEW + TUTORIAL_STEP_COMPLETION + AGENT_GRAPH_EXECUTION + AGENT_NODE_EXECUTION +} + +enum AggregationType { + COUNT + SUM + AVG + MAX + MIN + NO_AGGREGATION +} + +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 AnalyticsMetric + // 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? + // Data Aggregation Type + aggregationType AggregationType @default(NO_AGGREGATION) + // Aggregation Counter used for aggregation style events that beenefit from it. (AVG) (not self incrementing) + aggregationCounter Int? @default(1) + + // Link to User model + userId String + user User @relation(fields: [userId], references: [id]) + + // Allows us to have unique but useful user level metrics. + @@unique([analyticMetric, userId, dataString, aggregationType]) + @@index(fields: [analyticMetric, userId, dataString, aggregationType], name: "analytics_metric_index") +}