This is an automated email from the ASF dual-hosted git repository.

skrawcz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/burr.git


The following commit(s) were added to refs/heads/main by this push:
     new 226288fd feat: implement counter app demo in Burr UI (#675)
226288fd is described below

commit 226288fdb7438943a989c815641acb9ec322019f
Author: AndrĂ© Ahlert <[email protected]>
AuthorDate: Mon Mar 16 02:35:51 2026 -0300

    feat: implement counter app demo in Burr UI (#675)
    
    * feat: implement counter app demo in Burr UI (#69)
    
    Add a fully functional counter demo integrated with the Burr tracking
    UI, replacing the previous WIP placeholder.
    
    Backend:
    - Add FastAPI server for the counter example with /count, /state and
      /create endpoints
    - Register counter router in the main Burr tracking server
    
    Frontend:
    - Implement Counter React component with increment button and live
      telemetry view
    - Add CounterState model and API service methods
    
    * Fixes pre-commit issue
---
 burr/tracking/server/run.py                        |   2 +
 examples/hello-world-counter/server.py             | 117 +++++++++++++++++++++
 telemetry/ui/src/api/index.ts                      |   1 +
 .../Counter.tsx => api/models/CounterState.ts}     |  25 ++---
 telemetry/ui/src/api/services/DefaultService.ts    |  85 +++++++++++++++
 telemetry/ui/src/examples/Counter.tsx              | 101 +++++++++++++++---
 6 files changed, 300 insertions(+), 31 deletions(-)

diff --git a/burr/tracking/server/run.py b/burr/tracking/server/run.py
index 0f3303de..0e5ce62b 100644
--- a/burr/tracking/server/run.py
+++ b/burr/tracking/server/run.py
@@ -60,6 +60,7 @@ try:
     chatbot = 
importlib.import_module("burr.examples.multi-modal-chatbot.server")
     streaming_chatbot = 
importlib.import_module("burr.examples.streaming-fastapi.server")
     deep_researcher = 
importlib.import_module("burr.examples.deep-researcher.server")
+    counter = 
importlib.import_module("burr.examples.hello-world-counter.server")
 
 except ImportError as e:
     raise e
@@ -343,6 +344,7 @@ app.include_router(chatbot.router, prefix="/api/v0/chatbot")
 app.include_router(email_assistant.router, prefix="/api/v0/email_assistant")
 app.include_router(streaming_chatbot.router, 
prefix="/api/v0/streaming_chatbot")
 app.include_router(deep_researcher.router, prefix="/api/v0/deep_researcher")
+app.include_router(counter.router, prefix="/api/v0/counter")
 
 if SERVE_STATIC:
     BASE_ASSET_DIRECTORY = str(files("burr").joinpath("tracking/server/build"))
diff --git a/examples/hello-world-counter/server.py 
b/examples/hello-world-counter/server.py
new file mode 100644
index 00000000..4e9a7179
--- /dev/null
+++ b/examples/hello-world-counter/server.py
@@ -0,0 +1,117 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import functools
+import importlib
+
+import pydantic
+from fastapi import APIRouter
+
+from burr.core import Application, ApplicationBuilder, Result, default, expr
+from burr.tracking import LocalTrackingClient
+
+"""This file represents a simple counter API backed with Burr.
+We manage an application, write to it with post endpoints, and read with
+get/ endpoints.
+
+This demonstrates how you can build interactive web applications with Burr!
+"""
+
+counter_application = importlib.import_module(
+    "burr.examples.hello-world-counter.application"
+)  # noqa: F401
+
+router = APIRouter()
+
+COUNTER_LIMIT = 1000
+
+
+class CounterState(pydantic.BaseModel):
+    """Pydantic model for the counter state."""
+
+    counter: int
+    app_id: str
+
+
[email protected]_cache(maxsize=128)
+def _get_application(project_id: str, app_id: str) -> Application:
+    """Quick tool to get the application -- caches it"""
+    tracker = LocalTrackingClient(project=project_id, storage_dir="~/.burr")
+    return (
+        ApplicationBuilder()
+        .with_actions(
+            counter=counter_application.counter,
+            result=Result("counter"),
+        )
+        .with_transitions(
+            ("counter", "counter", expr(f"counter < {COUNTER_LIMIT}")),
+            ("counter", "result", default),
+        )
+        .initialize_from(
+            tracker,
+            resume_at_next_action=True,
+            default_state={"counter": 0},
+            default_entrypoint="counter",
+        )
+        .with_tracker(tracker)
+        .with_identifiers(app_id=app_id)
+        .build()
+    )
+
+
[email protected]("/count/{project_id}/{app_id}", response_model=CounterState)
+def count(project_id: str, app_id: str) -> CounterState:
+    """Increment the counter by one step and return the new state.
+
+    :param project_id: Project ID to run
+    :param app_id: Application ID to run
+    :return: The current counter state
+    """
+    burr_app = _get_application(project_id, app_id)
+    burr_app.step()
+    return CounterState(
+        counter=burr_app.state["counter"],
+        app_id=app_id,
+    )
+
+
[email protected]("/state/{project_id}/{app_id}", response_model=CounterState)
+def get_counter_state(project_id: str, app_id: str) -> CounterState:
+    """Get the current counter state without incrementing.
+
+    :param project_id: Project ID
+    :param app_id: App ID
+    :return: The current counter state
+    """
+    burr_app = _get_application(project_id, app_id)
+    return CounterState(
+        counter=burr_app.state["counter"],
+        app_id=app_id,
+    )
+
+
[email protected]("/create/{project_id}/{app_id}", response_model=str)
+async def create_new_application(project_id: str, app_id: str) -> str:
+    """Endpoint to create a new application -- used by the FE when
+    the user types in a new App ID
+
+    :param project_id: Project ID
+    :param app_id: App ID
+    :return: The app ID
+    """
+    _get_application(app_id=app_id, project_id=project_id)
+    return app_id
diff --git a/telemetry/ui/src/api/index.ts b/telemetry/ui/src/api/index.ts
index 0979c3fb..7e0972e4 100644
--- a/telemetry/ui/src/api/index.ts
+++ b/telemetry/ui/src/api/index.ts
@@ -41,6 +41,7 @@ export type { BackendSpec } from './models/BackendSpec';
 export type { BeginEntryModel } from './models/BeginEntryModel';
 export type { BeginSpanModel } from './models/BeginSpanModel';
 export { ChatItem } from './models/ChatItem';
+export type { CounterState } from './models/CounterState';
 export { ChildApplicationModel } from './models/ChildApplicationModel';
 export type { DraftInit } from './models/DraftInit';
 export { EmailAssistantState } from './models/EmailAssistantState';
diff --git a/telemetry/ui/src/examples/Counter.tsx 
b/telemetry/ui/src/api/models/CounterState.ts
similarity index 61%
copy from telemetry/ui/src/examples/Counter.tsx
copy to telemetry/ui/src/api/models/CounterState.ts
index 32231a1e..ba7de865 100644
--- a/telemetry/ui/src/examples/Counter.tsx
+++ b/telemetry/ui/src/api/models/CounterState.ts
@@ -17,22 +17,11 @@
  * under the License.
  */
 
-import { Link } from 'react-router-dom';
-
-export const Counter = () => {
-  return (
-    <div className="flex justify-center items-center h-full w-full">
-      <p className="text-gray-700">
-        {' '}
-        This is a WIP! Please check back soon or comment/vote/contribute at 
the{' '}
-        <Link
-          className="hover:underline text-dwlightblue"
-          to="https://github.com/DAGWorks-Inc/burr/issues/69";
-        >
-          github issue
-        </Link>
-        .
-      </p>
-    </div>
-  );
+/* generated using openapi-typescript-codegen -- do no edit */
+/* istanbul ignore file */
+/* tslint:disable */
+/* eslint-disable */
+export type CounterState = {
+  counter: number;
+  app_id: string;
 };
diff --git a/telemetry/ui/src/api/services/DefaultService.ts 
b/telemetry/ui/src/api/services/DefaultService.ts
index b069bf3e..34c1c697 100644
--- a/telemetry/ui/src/api/services/DefaultService.ts
+++ b/telemetry/ui/src/api/services/DefaultService.ts
@@ -36,6 +36,7 @@ import type { Project } from '../models/Project';
 import type { PromptInput } from '../models/PromptInput';
 import type { QuestionAnswers } from '../models/QuestionAnswers';
 import type { ResearchSummary } from '../models/ResearchSummary';
+import type { CounterState } from '../models/CounterState';
 import type { CancelablePromise } from '../core/CancelablePromise';
 import { OpenAPI } from '../core/OpenAPI';
 import { request as __request } from '../core/request';
@@ -697,4 +698,88 @@ export class DefaultService {
       url: '/api/v0/deep_researcher/validate'
     });
   }
+  /**
+   * Count
+   * Increment the counter by one step and return the new state.
+   *
+   * :param project_id: Project ID to run
+   * :param app_id: Application ID to run
+   * :return: The current counter state
+   * @param projectId
+   * @param appId
+   * @returns CounterState Successful Response
+   * @throws ApiError
+   */
+  public static countApiV0CounterCountProjectIdAppIdPost(
+    projectId: string,
+    appId: string
+  ): CancelablePromise<CounterState> {
+    return __request(OpenAPI, {
+      method: 'POST',
+      url: '/api/v0/counter/count/{project_id}/{app_id}',
+      path: {
+        project_id: projectId,
+        app_id: appId
+      },
+      errors: {
+        422: `Validation Error`
+      }
+    });
+  }
+  /**
+   * Get Counter State
+   * Get the current counter state without incrementing.
+   *
+   * :param project_id: Project ID
+   * :param app_id: App ID
+   * :return: The current counter state
+   * @param projectId
+   * @param appId
+   * @returns CounterState Successful Response
+   * @throws ApiError
+   */
+  public static getCounterStateApiV0CounterStateProjectIdAppIdGet(
+    projectId: string,
+    appId: string
+  ): CancelablePromise<CounterState> {
+    return __request(OpenAPI, {
+      method: 'GET',
+      url: '/api/v0/counter/state/{project_id}/{app_id}',
+      path: {
+        project_id: projectId,
+        app_id: appId
+      },
+      errors: {
+        422: `Validation Error`
+      }
+    });
+  }
+  /**
+   * Create New Application
+   * Endpoint to create a new counter application
+   *
+   * :param project_id: Project ID
+   * :param app_id: App ID
+   * :return: The app ID
+   * @param projectId
+   * @param appId
+   * @returns string Successful Response
+   * @throws ApiError
+   */
+  public static createNewApplicationApiV0CounterCreateProjectIdAppIdPost(
+    projectId: string,
+    appId: string
+  ): CancelablePromise<string> {
+    return __request(OpenAPI, {
+      method: 'POST',
+      url: '/api/v0/counter/create/{project_id}/{app_id}',
+      path: {
+        project_id: projectId,
+        app_id: appId
+      },
+      errors: {
+        422: `Validation Error`
+      }
+    });
+  }
 }
diff --git a/telemetry/ui/src/examples/Counter.tsx 
b/telemetry/ui/src/examples/Counter.tsx
index 32231a1e..518657b7 100644
--- a/telemetry/ui/src/examples/Counter.tsx
+++ b/telemetry/ui/src/examples/Counter.tsx
@@ -17,22 +17,97 @@
  * under the License.
  */
 
-import { Link } from 'react-router-dom';
+import { Button } from '../components/common/button';
+import { TwoColumnLayout } from '../components/common/layout';
+import { ApplicationSummary, DefaultService } from '../api';
+import { useState } from 'react';
+import { useMutation, useQuery } from 'react-query';
+import { Loading } from '../components/common/loading';
+import { TelemetryWithSelector } from './Common';
+
+const CounterApp = (props: { projectId: string; appId: string | undefined }) 
=> {
+  const [counterValue, setCounterValue] = useState<number>(0);
+
+  const { isLoading } = useQuery(
+    ['counter-state', props.projectId, props.appId],
+    () =>
+      DefaultService.getCounterStateApiV0CounterStateProjectIdAppIdGet(
+        props.projectId,
+        props.appId || ''
+      ),
+    {
+      enabled: props.appId !== undefined,
+      onSuccess: (data) => {
+        setCounterValue(data.counter);
+      }
+    }
+  );
+
+  const mutation = useMutation(
+    () => {
+      return DefaultService.countApiV0CounterCountProjectIdAppIdPost(
+        props.projectId,
+        props.appId || ''
+      );
+    },
+    {
+      onSuccess: (data) => {
+        setCounterValue(data.counter);
+      }
+    }
+  );
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  const isWaiting = mutation.isLoading;
 
-export const Counter = () => {
   return (
-    <div className="flex justify-center items-center h-full w-full">
-      <p className="text-gray-700">
-        {' '}
-        This is a WIP! Please check back soon or comment/vote/contribute at 
the{' '}
-        <Link
-          className="hover:underline text-dwlightblue"
-          to="https://github.com/DAGWorks-Inc/burr/issues/69";
+    <div className="mr-4 bg-white w-full flex flex-col h-full">
+      <h1 className="text-2xl font-bold px-4 text-gray-600">Counter Demo</h1>
+      <h2 className="text-lg font-normal px-4 text-gray-500">
+        A simple counter powered by Burr. Click the button to increment and 
watch the state machine
+        on the right.
+      </h2>
+      <div className="flex-1 flex flex-col items-center justify-center gap-8">
+        <div className="text-8xl font-bold text-gray-700">{counterValue}</div>
+        <Button
+          className="w-40 cursor-pointer text-lg"
+          color="dark"
+          disabled={isWaiting || props.appId === undefined}
+          onClick={() => mutation.mutate()}
         >
-          github issue
-        </Link>
-        .
-      </p>
+          {isWaiting ? 'Counting...' : 'Count +1'}
+        </Button>
+        {props.appId === undefined && (
+          <p className="text-gray-400 text-sm">
+            Select or create a counter from the panel on the right to get 
started.
+          </p>
+        )}
+      </div>
     </div>
   );
 };
+
+export const Counter = () => {
+  const currentProject = 'demo_counter';
+  const [currentApp, setCurrentApp] = useState<ApplicationSummary | 
undefined>(undefined);
+
+  return (
+    <TwoColumnLayout
+      firstItem={<CounterApp projectId={currentProject} 
appId={currentApp?.app_id} />}
+      secondItem={
+        <TelemetryWithSelector
+          projectId={currentProject}
+          currentApp={currentApp}
+          setCurrentApp={setCurrentApp}
+          createNewApp={
+            
DefaultService.createNewApplicationApiV0CounterCreateProjectIdAppIdPost
+          }
+        />
+      }
+      mode={'third'}
+    />
+  );
+};

Reply via email to