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'}
+ />
+ );
+};