Skip to content

Commit 226288f

Browse files
authored
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
1 parent 241d6ee commit 226288f

6 files changed

Lines changed: 320 additions & 13 deletions

File tree

burr/tracking/server/run.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
chatbot = importlib.import_module("burr.examples.multi-modal-chatbot.server")
6161
streaming_chatbot = importlib.import_module("burr.examples.streaming-fastapi.server")
6262
deep_researcher = importlib.import_module("burr.examples.deep-researcher.server")
63+
counter = importlib.import_module("burr.examples.hello-world-counter.server")
6364

6465
except ImportError as e:
6566
raise e
@@ -343,6 +344,7 @@ async def version() -> dict:
343344
app.include_router(email_assistant.router, prefix="/api/v0/email_assistant")
344345
app.include_router(streaming_chatbot.router, prefix="/api/v0/streaming_chatbot")
345346
app.include_router(deep_researcher.router, prefix="/api/v0/deep_researcher")
347+
app.include_router(counter.router, prefix="/api/v0/counter")
346348

347349
if SERVE_STATIC:
348350
BASE_ASSET_DIRECTORY = str(files("burr").joinpath("tracking/server/build"))
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import functools
19+
import importlib
20+
21+
import pydantic
22+
from fastapi import APIRouter
23+
24+
from burr.core import Application, ApplicationBuilder, Result, default, expr
25+
from burr.tracking import LocalTrackingClient
26+
27+
"""This file represents a simple counter API backed with Burr.
28+
We manage an application, write to it with post endpoints, and read with
29+
get/ endpoints.
30+
31+
This demonstrates how you can build interactive web applications with Burr!
32+
"""
33+
34+
counter_application = importlib.import_module(
35+
"burr.examples.hello-world-counter.application"
36+
) # noqa: F401
37+
38+
router = APIRouter()
39+
40+
COUNTER_LIMIT = 1000
41+
42+
43+
class CounterState(pydantic.BaseModel):
44+
"""Pydantic model for the counter state."""
45+
46+
counter: int
47+
app_id: str
48+
49+
50+
@functools.lru_cache(maxsize=128)
51+
def _get_application(project_id: str, app_id: str) -> Application:
52+
"""Quick tool to get the application -- caches it"""
53+
tracker = LocalTrackingClient(project=project_id, storage_dir="~/.burr")
54+
return (
55+
ApplicationBuilder()
56+
.with_actions(
57+
counter=counter_application.counter,
58+
result=Result("counter"),
59+
)
60+
.with_transitions(
61+
("counter", "counter", expr(f"counter < {COUNTER_LIMIT}")),
62+
("counter", "result", default),
63+
)
64+
.initialize_from(
65+
tracker,
66+
resume_at_next_action=True,
67+
default_state={"counter": 0},
68+
default_entrypoint="counter",
69+
)
70+
.with_tracker(tracker)
71+
.with_identifiers(app_id=app_id)
72+
.build()
73+
)
74+
75+
76+
@router.post("/count/{project_id}/{app_id}", response_model=CounterState)
77+
def count(project_id: str, app_id: str) -> CounterState:
78+
"""Increment the counter by one step and return the new state.
79+
80+
:param project_id: Project ID to run
81+
:param app_id: Application ID to run
82+
:return: The current counter state
83+
"""
84+
burr_app = _get_application(project_id, app_id)
85+
burr_app.step()
86+
return CounterState(
87+
counter=burr_app.state["counter"],
88+
app_id=app_id,
89+
)
90+
91+
92+
@router.get("/state/{project_id}/{app_id}", response_model=CounterState)
93+
def get_counter_state(project_id: str, app_id: str) -> CounterState:
94+
"""Get the current counter state without incrementing.
95+
96+
:param project_id: Project ID
97+
:param app_id: App ID
98+
:return: The current counter state
99+
"""
100+
burr_app = _get_application(project_id, app_id)
101+
return CounterState(
102+
counter=burr_app.state["counter"],
103+
app_id=app_id,
104+
)
105+
106+
107+
@router.post("/create/{project_id}/{app_id}", response_model=str)
108+
async def create_new_application(project_id: str, app_id: str) -> str:
109+
"""Endpoint to create a new application -- used by the FE when
110+
the user types in a new App ID
111+
112+
:param project_id: Project ID
113+
:param app_id: App ID
114+
:return: The app ID
115+
"""
116+
_get_application(app_id=app_id, project_id=project_id)
117+
return app_id

telemetry/ui/src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type { BackendSpec } from './models/BackendSpec';
4141
export type { BeginEntryModel } from './models/BeginEntryModel';
4242
export type { BeginSpanModel } from './models/BeginSpanModel';
4343
export { ChatItem } from './models/ChatItem';
44+
export type { CounterState } from './models/CounterState';
4445
export { ChildApplicationModel } from './models/ChildApplicationModel';
4546
export type { DraftInit } from './models/DraftInit';
4647
export { EmailAssistantState } from './models/EmailAssistantState';
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
/* generated using openapi-typescript-codegen -- do no edit */
21+
/* istanbul ignore file */
22+
/* tslint:disable */
23+
/* eslint-disable */
24+
export type CounterState = {
25+
counter: number;
26+
app_id: string;
27+
};

telemetry/ui/src/api/services/DefaultService.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import type { Project } from '../models/Project';
3636
import type { PromptInput } from '../models/PromptInput';
3737
import type { QuestionAnswers } from '../models/QuestionAnswers';
3838
import type { ResearchSummary } from '../models/ResearchSummary';
39+
import type { CounterState } from '../models/CounterState';
3940
import type { CancelablePromise } from '../core/CancelablePromise';
4041
import { OpenAPI } from '../core/OpenAPI';
4142
import { request as __request } from '../core/request';
@@ -697,4 +698,88 @@ export class DefaultService {
697698
url: '/api/v0/deep_researcher/validate'
698699
});
699700
}
701+
/**
702+
* Count
703+
* Increment the counter by one step and return the new state.
704+
*
705+
* :param project_id: Project ID to run
706+
* :param app_id: Application ID to run
707+
* :return: The current counter state
708+
* @param projectId
709+
* @param appId
710+
* @returns CounterState Successful Response
711+
* @throws ApiError
712+
*/
713+
public static countApiV0CounterCountProjectIdAppIdPost(
714+
projectId: string,
715+
appId: string
716+
): CancelablePromise<CounterState> {
717+
return __request(OpenAPI, {
718+
method: 'POST',
719+
url: '/api/v0/counter/count/{project_id}/{app_id}',
720+
path: {
721+
project_id: projectId,
722+
app_id: appId
723+
},
724+
errors: {
725+
422: `Validation Error`
726+
}
727+
});
728+
}
729+
/**
730+
* Get Counter State
731+
* Get the current counter state without incrementing.
732+
*
733+
* :param project_id: Project ID
734+
* :param app_id: App ID
735+
* :return: The current counter state
736+
* @param projectId
737+
* @param appId
738+
* @returns CounterState Successful Response
739+
* @throws ApiError
740+
*/
741+
public static getCounterStateApiV0CounterStateProjectIdAppIdGet(
742+
projectId: string,
743+
appId: string
744+
): CancelablePromise<CounterState> {
745+
return __request(OpenAPI, {
746+
method: 'GET',
747+
url: '/api/v0/counter/state/{project_id}/{app_id}',
748+
path: {
749+
project_id: projectId,
750+
app_id: appId
751+
},
752+
errors: {
753+
422: `Validation Error`
754+
}
755+
});
756+
}
757+
/**
758+
* Create New Application
759+
* Endpoint to create a new counter application
760+
*
761+
* :param project_id: Project ID
762+
* :param app_id: App ID
763+
* :return: The app ID
764+
* @param projectId
765+
* @param appId
766+
* @returns string Successful Response
767+
* @throws ApiError
768+
*/
769+
public static createNewApplicationApiV0CounterCreateProjectIdAppIdPost(
770+
projectId: string,
771+
appId: string
772+
): CancelablePromise<string> {
773+
return __request(OpenAPI, {
774+
method: 'POST',
775+
url: '/api/v0/counter/create/{project_id}/{app_id}',
776+
path: {
777+
project_id: projectId,
778+
app_id: appId
779+
},
780+
errors: {
781+
422: `Validation Error`
782+
}
783+
});
784+
}
700785
}

telemetry/ui/src/examples/Counter.tsx

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,97 @@
1717
* under the License.
1818
*/
1919

20-
import { Link } from 'react-router-dom';
20+
import { Button } from '../components/common/button';
21+
import { TwoColumnLayout } from '../components/common/layout';
22+
import { ApplicationSummary, DefaultService } from '../api';
23+
import { useState } from 'react';
24+
import { useMutation, useQuery } from 'react-query';
25+
import { Loading } from '../components/common/loading';
26+
import { TelemetryWithSelector } from './Common';
27+
28+
const CounterApp = (props: { projectId: string; appId: string | undefined }) => {
29+
const [counterValue, setCounterValue] = useState<number>(0);
30+
31+
const { isLoading } = useQuery(
32+
['counter-state', props.projectId, props.appId],
33+
() =>
34+
DefaultService.getCounterStateApiV0CounterStateProjectIdAppIdGet(
35+
props.projectId,
36+
props.appId || ''
37+
),
38+
{
39+
enabled: props.appId !== undefined,
40+
onSuccess: (data) => {
41+
setCounterValue(data.counter);
42+
}
43+
}
44+
);
45+
46+
const mutation = useMutation(
47+
() => {
48+
return DefaultService.countApiV0CounterCountProjectIdAppIdPost(
49+
props.projectId,
50+
props.appId || ''
51+
);
52+
},
53+
{
54+
onSuccess: (data) => {
55+
setCounterValue(data.counter);
56+
}
57+
}
58+
);
59+
60+
if (isLoading) {
61+
return <Loading />;
62+
}
63+
64+
const isWaiting = mutation.isLoading;
2165

22-
export const Counter = () => {
2366
return (
24-
<div className="flex justify-center items-center h-full w-full">
25-
<p className="text-gray-700">
26-
{' '}
27-
This is a WIP! Please check back soon or comment/vote/contribute at the{' '}
28-
<Link
29-
className="hover:underline text-dwlightblue"
30-
to="https://github.com/DAGWorks-Inc/burr/issues/69"
67+
<div className="mr-4 bg-white w-full flex flex-col h-full">
68+
<h1 className="text-2xl font-bold px-4 text-gray-600">Counter Demo</h1>
69+
<h2 className="text-lg font-normal px-4 text-gray-500">
70+
A simple counter powered by Burr. Click the button to increment and watch the state machine
71+
on the right.
72+
</h2>
73+
<div className="flex-1 flex flex-col items-center justify-center gap-8">
74+
<div className="text-8xl font-bold text-gray-700">{counterValue}</div>
75+
<Button
76+
className="w-40 cursor-pointer text-lg"
77+
color="dark"
78+
disabled={isWaiting || props.appId === undefined}
79+
onClick={() => mutation.mutate()}
3180
>
32-
github issue
33-
</Link>
34-
.
35-
</p>
81+
{isWaiting ? 'Counting...' : 'Count +1'}
82+
</Button>
83+
{props.appId === undefined && (
84+
<p className="text-gray-400 text-sm">
85+
Select or create a counter from the panel on the right to get started.
86+
</p>
87+
)}
88+
</div>
3689
</div>
3790
);
3891
};
92+
93+
export const Counter = () => {
94+
const currentProject = 'demo_counter';
95+
const [currentApp, setCurrentApp] = useState<ApplicationSummary | undefined>(undefined);
96+
97+
return (
98+
<TwoColumnLayout
99+
firstItem={<CounterApp projectId={currentProject} appId={currentApp?.app_id} />}
100+
secondItem={
101+
<TelemetryWithSelector
102+
projectId={currentProject}
103+
currentApp={currentApp}
104+
setCurrentApp={setCurrentApp}
105+
createNewApp={
106+
DefaultService.createNewApplicationApiV0CounterCreateProjectIdAppIdPost
107+
}
108+
/>
109+
}
110+
mode={'third'}
111+
/>
112+
);
113+
};

0 commit comments

Comments
 (0)