Skip to content

Commit

Permalink
feat: Listbuffer (for chatbotcard). #1944 #1928 (#2017)
Browse files Browse the repository at this point in the history
  • Loading branch information
mturoci authored Jun 16, 2023
1 parent eb94c56 commit 77be798
Show file tree
Hide file tree
Showing 21 changed files with 356 additions and 157 deletions.
3 changes: 3 additions & 0 deletions card.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ func loadBuf(ns *Namespace, b BufD) Buf {
if b.M != nil {
return loadMapBuf(ns, b.M)
}
if b.L != nil {
return loadListBuf(ns, b.L)
}
return nil
}

Expand Down
86 changes: 86 additions & 0 deletions listbuf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2020 H2O.ai, Inc.
//
// Licensed 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.

package wave

import "strconv"

// ListBuf represents a list (dynamic array) buffer.
type ListBuf struct {
b *FixBuf
i int
}

func (b *ListBuf) put(ixs any) {
if xs, ok := ixs.([]any); ok {
for _, x := range xs {
b.set("", x)
}
}
}

func (b *ListBuf) set(key string, v any) {
fb := b.b
// Check if key is a valid index.
if i, err := strconv.Atoi(key); err == nil {
// Support negative indices.
if i < 0 {
i += b.i
}
if i >= 0 && i < len(fb.tups) {
fb.seti(i, v)
return
}
}
// Otherwise, append to the current end.
if b.i >= len(fb.tups) {
xs := make([][]interface{}, len(fb.tups)*2)
tups := fb.tups
fb.tups = xs

for i, t := range tups {
fb.seti(i, t)
}
}
fb.seti(b.i, v)
b.i++
}

func (b *ListBuf) get(key string) (Cur, bool) {
// Check if key is a valid index.
if i, err := strconv.Atoi(key); err == nil {
if i < 0 {
i += len(b.b.tups)
}
return b.b.geti(i)
}

return b.b.geti(b.i)
}

func (b *ListBuf) dump() BufD {
return BufD{L: &ListBufD{b.b.t.f, b.b.tups, len(b.b.tups)}}
}

func loadListBuf(ns *Namespace, b *ListBufD) *ListBuf {
t := ns.make(b.F)
if len(b.D) == 0 {
n := b.N
if n <= 0 {
n = 10
}
return &ListBuf{newFixBuf(t, n), n - 1}
}
return &ListBuf{&FixBuf{t, b.D}, len(b.D)}
}
14 changes: 11 additions & 3 deletions protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type OpD struct {
C *CycBufD `json:"c,omitempty"` // value
F *FixBufD `json:"f,omitempty"` // value
M *MapBufD `json:"m,omitempty"` // value
L *ListBufD `json:"l,omitempty"` // value
D map[string]interface{} `json:"d,omitempty"` // card data
B []BufD `json:"b,omitempty"` // card buffers
}
Expand All @@ -55,9 +56,10 @@ type CardD struct {

// BufD represents the marshaled data for a buffer. This is a discriminated union.
type BufD struct {
C *CycBufD `json:"c,omitempty"`
F *FixBufD `json:"f,omitempty"`
M *MapBufD `json:"m,omitempty"`
C *CycBufD `json:"c,omitempty"`
F *FixBufD `json:"f,omitempty"`
M *MapBufD `json:"m,omitempty"`
L *ListBufD `json:"l,omitempty"`
}

// MapBufD represents the marshaled data for a MapBuf.
Expand All @@ -81,6 +83,12 @@ type CycBufD struct {
I int `json:"i"` // index
}

type ListBufD struct {
F []string `json:"f"` // fields
D [][]interface{} `json:"d"` // tuples
N int `json:"n"` // size
}

// AppRequest represents a request from an app.
type AppRequest struct {
RegisterApp *RegisterApp `json:"register_app,omitempty"`
Expand Down
17 changes: 6 additions & 11 deletions py/examples/chatbot.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
# Chatbot
# Use this card for chat interactions.
# #chat
# Use this card for chatbot interactions.
# #chatbot
# ---
from h2o_wave import main, app, Q, ui, data


MAX_MESSAGES = 500


@app('/demo')
async def serve(q: Q):
if not q.client.initialized:
# Cyclic buffer drops oldest messages when full. Must have exactly 2 columns - msg and fromUser.
cyclic_buffer = data(fields='msg fromUser', size=-MAX_MESSAGES)
q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=cyclic_buffer, name='chatbot')
q.page['meta'] = ui.meta_card(box='', theme='h2o-dark')
# List buffer is a dynamic array. Cyclic buffer can also be used. Must have exactly 2 fields - msg and fromUser.
q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=data('msg fromUser', t='list'), name='chatbot')
q.client.initialized = True

# A new message arrived.
if q.args.chatbot:
# Append user message.
q.page['example'].data[-1] = [q.args.chatbot, True]
q.page['example'].data += [q.args.chatbot, True]
# Append bot response.
q.page['example'].data[-1] = ['I am a fake chatbot. Sorry, I cannot help you.', False]
q.page['example'].data += ['I am a fake chatbot. Sorry, I cannot help you.', False]

await q.page.save()
50 changes: 24 additions & 26 deletions py/examples/chatbot_events_stop.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,48 @@
# Chatbot
# Use this card for chat interactions.
# #chat
# Use this card for chatbot interactions. Streaming can be stopped by the user.
# #chatbot
# ---
from h2o_wave import main, app, Q, ui, data
import asyncio

async def stream_message(q, msg):

async def stream_message(q):
stream = ''
q.page['example'].data += [stream, False]
# Show the "Stop generating" button
q.page['example'].generating = True
for w in msg.split():
for w in 'I am a fake chatbot. Sorry, I cannot help you.'.split():
await asyncio.sleep(0.3)
stream += w + ' '
q.page['example'].data[q.client.msg_num] = [stream, False]
q.page['example'].data[-1] = [stream, False]
await q.page.save()
# Hide the "Stop generating" button
q.page['example'].generating = False
await q.page.save()


@app('/demo')
async def serve(q: Q):
if not q.client.initialized:
# Use map buffer with sortable keys to store messages - allows indexing + streaming the chat messages.
map_buffer = data(fields='msg fromUser')
q.client.msg_num = 0
q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=map_buffer, name='chatbot', events=['stop'])
q.page['meta'] = ui.meta_card(box='', theme='h2o-dark')
q.page['example'] = ui.chatbot_card(
box='1 1 5 5',
data=data(fields='msg fromUser', t='list'),
name='chatbot',
events=['stop']
)
q.client.initialized = True

# Check if we get a stop event
if q.events.chatbot:
if q.events.chatbot.stop:
# Cancel the streaming task
q.client.task.cancel()
# Hide the "Stop generating" button
q.page['example'].generating = False
# Handle the stop event.
if q.events.chatbot and q.events.chatbot.stop:
# Cancel the streaming task.
q.client.task.cancel()
# Hide the "Stop generating" button.
q.page['example'].generating = False
# A new message arrived.
elif q.args.chatbot:
# Append user message.
q.client.msg_num += 1
q.page['example'].data[q.client.msg_num] = [q.args.chatbot, True]
q.page['example'].data += [q.args.chatbot, True]
# Run the streaming within cancelable asyncio task.
q.client.task = asyncio.create_task(stream_message(q))

# Stream bot response.
q.client.msg_num += 1
chatbot_response = 'I am a fake chatbot. Sorry, I cannot help you.'
# Create and run a task to stream the message
q.client.task = asyncio.create_task(stream_message(q, chatbot_response))

await q.page.save()
await q.page.save()
30 changes: 30 additions & 0 deletions py/examples/chatbot_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Chatbot
# Use this card for chatbot interactions, supports text streaming.
# #chatbot
# ---
from h2o_wave import main, app, Q, ui, data


@app('/demo')
async def serve(q: Q):
if not q.client.initialized:
# Use list buffer to allow easy streaming. Must have exactly 2 fields - msg and fromUser.
q.page['example'] = ui.chatbot_card(box='1 1 5 5', data=data(fields='msg fromUser', t='list'), name='chatbot')
q.client.initialized = True

# A new message arrived.
if q.args.chatbot:
# Append user message.
q.page['example'].data += [q.args.chatbot, True]
# Append bot response.
q.page['example'].data += ['', False]

# Stream bot response.
stream = ''
for w in 'I am a fake chatbot. Sorry, I cannot help you.'.split():
await q.sleep(0.1)
stream += w + ' '
q.page['example'].data[-1] = [stream, False]
await q.page.save()

await q.page.save()
2 changes: 1 addition & 1 deletion py/examples/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ opencv-python==4.5.5.64
pandas==1.3.5
plotly==5.7.0
pygments==2.11.2
scikit-learn==1.0.2
scikit-learn==1.2.2
toml==0.10.2
vega-datasets==0.9.0
1 change: 1 addition & 0 deletions py/examples/tour.conf
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ layout.py
layout_size.py
layout_responsive.py
chatbot.py
chatbot_stream.py
chatbot_events_stop.py
form.py
form_visibility.py
Expand Down
Loading

0 comments on commit 77be798

Please sign in to comment.