Skip to content

Commit 2ff5b35

Browse files
committed
feat: redux of function calling and adds context example
1 parent 71e0c02 commit 2ff5b35

File tree

10 files changed

+768
-24
lines changed

10 files changed

+768
-24
lines changed

examples/agent/websocket/context-history/main.go

Lines changed: 572 additions & 0 deletions
Large diffs are not rendered by default.

pkg/api/agent/v1/websocket/chan_default.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ func NewDefaultChanHandler() *DefaultChanHandler {
4646
closeChan: make(chan *interfaces.CloseResponse),
4747
errorChan: make(chan *interfaces.ErrorResponse),
4848
unhandledChan: make(chan *[]byte),
49+
historyConversationTextChan: make(chan *interfaces.HistoryConversationText),
50+
historyFunctionCallsChan: make(chan *interfaces.HistoryFunctionCalls),
4951
}
5052

5153
go func() {
@@ -133,6 +135,16 @@ func (dch DefaultChanHandler) GetSettingsApplied() []*chan *interfaces.SettingsA
133135
return []*chan *interfaces.SettingsAppliedResponse{&dch.settingsAppliedResponse}
134136
}
135137

138+
// GetHistoryConversationText returns the history conversation text channels
139+
func (dch DefaultChanHandler) GetHistoryConversationText() []*chan *interfaces.HistoryConversationText {
140+
return []*chan *interfaces.HistoryConversationText{&dch.historyConversationTextChan}
141+
}
142+
143+
// GetHistoryFunctionCalls returns the history function calls channels
144+
func (dch DefaultChanHandler) GetHistoryFunctionCalls() []*chan *interfaces.HistoryFunctionCalls {
145+
return []*chan *interfaces.HistoryFunctionCalls{&dch.historyFunctionCallsChan}
146+
}
147+
136148
// Open is the callback for when the connection opens
137149
//
138150
//nolint:funlen,gocyclo // this is a complex function. keep as is

pkg/api/agent/v1/websocket/chan_router.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ func NewChanRouter(chans interfaces.AgentMessageChan) *ChanRouter {
5555
closeChan: make([]*chan *interfaces.CloseResponse, 0),
5656
errorChan: make([]*chan *interfaces.ErrorResponse, 0),
5757
unhandledChan: make([]*chan *[]byte, 0),
58+
historyConversationTextChan: make([]*chan *interfaces.HistoryConversationText, 0),
59+
historyFunctionCallsChan: make([]*chan *interfaces.HistoryFunctionCalls, 0),
5860
}
5961

6062
if chans != nil {
63+
// CORE FUNCTIONALITY: Always available via AgentMessageChan interface
6164
router.binaryChan = append(router.binaryChan, chans.GetBinary()...)
6265
router.openChan = append(router.openChan, chans.GetOpen()...)
6366
router.welcomeResponse = append(router.welcomeResponse, chans.GetWelcome()...)
@@ -73,6 +76,18 @@ func NewChanRouter(chans interfaces.AgentMessageChan) *ChanRouter {
7376
router.injectionRefusedResponse = append(router.injectionRefusedResponse, chans.GetInjectionRefused()...)
7477
router.keepAliveResponse = append(router.keepAliveResponse, chans.GetKeepAlive()...)
7578
router.settingsAppliedResponse = append(router.settingsAppliedResponse, chans.GetSettingsApplied()...)
79+
80+
// INTERFACE SEGREGATION: Optional History capability detection
81+
82+
if historyChan, hasHistory := chans.(interfaces.HistoryMessageChan); hasHistory {
83+
klog.V(4).Infof("Handler supports History functionality - routing History messages\n")
84+
router.historyConversationTextChan = append(router.historyConversationTextChan, historyChan.GetHistoryConversationText()...)
85+
router.historyFunctionCallsChan = append(router.historyFunctionCallsChan, historyChan.GetHistoryFunctionCalls()...)
86+
} else {
87+
klog.V(4).Infof("Handler does not implement HistoryMessageChan - History messages will route to UnhandledMessage\n")
88+
}
89+
// Note: If handler doesn't implement HistoryMessageChan, History messages will
90+
// be routed to UnhandledMessage, which is the expected fallback behavior.
7691
}
7792

7893
return router
@@ -354,6 +369,33 @@ func (r *ChanRouter) processSettingsApplied(byMsg []byte) error {
354369
return r.processGeneric(string(interfaces.TypeSettingsAppliedResponse), byMsg, action)
355370
}
356371

372+
func (r *ChanRouter) processHistory(byMsg []byte) error {
373+
// Try to parse as HistoryConversationText first
374+
var convHistory interfaces.HistoryConversationText
375+
if err := json.Unmarshal(byMsg, &convHistory); err == nil {
376+
if convHistory.Role != "" && convHistory.Content != "" {
377+
for _, ch := range r.historyConversationTextChan {
378+
*ch <- &convHistory
379+
}
380+
return nil
381+
}
382+
}
383+
384+
// Try to parse as HistoryFunctionCalls
385+
var funcHistory interfaces.HistoryFunctionCalls
386+
if err := json.Unmarshal(byMsg, &funcHistory); err == nil {
387+
if len(funcHistory.FunctionCalls) > 0 {
388+
for _, ch := range r.historyFunctionCallsChan {
389+
*ch <- &funcHistory
390+
}
391+
return nil
392+
}
393+
}
394+
395+
klog.V(1).Infof("processHistory: Unable to parse History message")
396+
return ErrInvalidMessageType
397+
}
398+
357399
// Message handles platform messages and routes them appropriately based on the MessageType
358400
func (r *ChanRouter) Message(byMsg []byte) error {
359401
klog.V(6).Infof("router.Message ENTER\n")
@@ -393,6 +435,8 @@ func (r *ChanRouter) Message(byMsg []byte) error {
393435
err = r.processKeepAlive(byMsg)
394436
case interfaces.TypeSettingsAppliedResponse:
395437
err = r.processSettingsApplied(byMsg)
438+
case "History":
439+
err = r.processHistory(byMsg)
396440
default:
397441
err = r.UnhandledMessage(byMsg)
398442
}

pkg/api/agent/v1/websocket/interfaces/interfaces.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@
55
// This package defines interfaces for the live API
66
package interfacesv1
77

8-
/*
9-
Chan Interfaces
10-
*/
11-
// AgentMessageChan is a callback used to receive notifcations for platforms messages
8+
// AgentMessageChan is the core interface for receiving agent message notifications.
9+
// This interface should remain unchanged to maintain backwards compatibility.
1210
type AgentMessageChan interface {
1311
GetBinary() []*chan *[]byte
1412
GetOpen() []*chan *OpenResponse
@@ -26,3 +24,10 @@ type AgentMessageChan interface {
2624
GetKeepAlive() []*chan *KeepAlive
2725
GetSettingsApplied() []*chan *SettingsAppliedResponse
2826
}
27+
28+
// HistoryMessageChan is an optional interface for receiving History message notifications.
29+
// Implement this interface in addition to AgentMessageChan to be non-breaking.
30+
type HistoryMessageChan interface {
31+
GetHistoryConversationText() []*chan *HistoryConversationText
32+
GetHistoryFunctionCalls() []*chan *HistoryFunctionCalls
33+
}

pkg/api/agent/v1/websocket/interfaces/types.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ type InjectUserMessage struct {
4040

4141
// FunctionCallResponse is the response from a function call
4242
type FunctionCallResponse struct {
43-
Type string `json:"type,omitempty"`
44-
FunctionCallID string `json:"function_call_id,omitempty"`
45-
Output string `json:"output,omitempty"`
43+
Type string `json:"type,omitempty"`
44+
ID string `json:"id,omitempty"` // Function call ID
45+
Name string `json:"name,omitempty"` // Function name
46+
Content string `json:"content,omitempty"` // Response content
4647
}
4748

4849
// HistoryConversationText is the request to send conversation history
@@ -130,10 +131,8 @@ type AgentThinkingResponse struct {
130131

131132
// FunctionCallRequestResponse is the response from a function call request
132133
type FunctionCallRequestResponse struct {
133-
Type string `json:"type,omitempty"`
134-
FunctionName string `json:"function_name,omitempty"`
135-
FunctionCallID string `json:"function_call_id,omitempty"`
136-
Input map[string]string `json:"input,omitempty"` // TODO: this is still undefined
134+
Type string `json:"type,omitempty"`
135+
Functions []FunctionCall `json:"functions,omitempty"`
137136
}
138137

139138
// AgentStartedSpeakingResponse is the response from the Agent starting to speak. You will ONLY get this if `experimental` is set to true.

pkg/api/agent/v1/websocket/types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type DefaultChanHandler struct {
3232
closeChan chan *interfaces.CloseResponse
3333
errorChan chan *interfaces.ErrorResponse
3434
unhandledChan chan *[]byte
35+
historyConversationTextChan chan *interfaces.HistoryConversationText
36+
historyFunctionCallsChan chan *interfaces.HistoryFunctionCalls
3537
}
3638

3739
// ChanRouter routes events
@@ -54,4 +56,6 @@ type ChanRouter struct {
5456
closeChan []*chan *interfaces.CloseResponse
5557
errorChan []*chan *interfaces.ErrorResponse
5658
unhandledChan []*chan *[]byte
59+
historyConversationTextChan []*chan *interfaces.HistoryConversationText
60+
historyFunctionCallsChan []*chan *interfaces.HistoryFunctionCalls
5761
}

pkg/client/interfaces/v1/types-agent.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,10 @@ type Endpoint struct {
4646
Headers map[string]string `json:"headers,omitempty"`
4747
Method string `json:"method,omitempty"`
4848
}
49-
type Item struct {
50-
Type string `json:"type,omitempty"`
51-
Description string `json:"description,omitempty"`
52-
}
53-
type Properties struct {
54-
Item Item `json:"item,omitempty"`
55-
}
5649
type Parameters struct {
57-
Type string `json:"type,omitempty"`
58-
Properties Properties `json:"properties,omitempty"`
59-
Required []string `json:"required,omitempty"`
50+
Type string `json:"type,omitempty"`
51+
Properties map[string]interface{} `json:"properties,omitempty"`
52+
Required []string `json:"required,omitempty"`
6053
}
6154
type Headers struct {
6255
Key string `json:"key,omitempty"`
@@ -66,7 +59,7 @@ type Functions struct {
6659
Name string `json:"name,omitempty"`
6760
Description string `json:"description,omitempty"`
6861
Parameters Parameters `json:"parameters,omitempty"`
69-
Endpoint Endpoint `json:"endpoint,omitempty"`
62+
Endpoint *Endpoint `json:"endpoint,omitempty"` // Pointer allows nil/omitempty for client-side functions
7063
}
7164
type Listen struct {
7265
Provider map[string]interface{} `json:"provider,omitempty"`
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"metadata":{"transaction_key":"deprecated","request_id":"4b9f7407-0dcf-4621-b4bc-5d9a7efcb8eb","sha256":"5324da68ede209a16ac69a38e8cd29cee4d754434a041166cda3a1f5e0b24566","created":"2025-07-24T18:58:58.305Z","duration":17.566313,"channels":1,"models":["1abfe86b-e047-4eed-858a-35e5625b41ee"],"model_info":{"1abfe86b-e047-4eed-858a-35e5625b41ee":{"name":"2-general-nova","version":"2024-01-06.5664","arch":"nova-2"}},"summary_info":{"model_uuid":"67875a7f-c9c4-48a0-aa55-5bdb8a91c34a"}},"results":{"channels":[{"alternatives":[{"transcript":"Yep. I said it before, and I'll say it again. Life moves pretty fast. You don't stop and look around once in a while, you could miss it.","confidence":0.99953604,"words":[{"word":"yep","start":5.6,"end":6.1,"confidence":0.9976603,"punctuated_word":"Yep."},{"word":"i","start":7.04,"end":7.2799997,"confidence":0.70802623,"punctuated_word":"I"},{"word":"said","start":7.2799997,"end":7.44,"confidence":0.965023,"punctuated_word":"said"},{"word":"it","start":7.44,"end":7.6,"confidence":0.99953604,"punctuated_word":"it"},{"word":"before","start":7.6,"end":7.9199996,"confidence":0.816449,"punctuated_word":"before,"},{"word":"and","start":7.9199996,"end":8.08,"confidence":0.9998956,"punctuated_word":"and"},{"word":"i'll","start":8.08,"end":8.24,"confidence":0.99988514,"punctuated_word":"I'll"},{"word":"say","start":8.24,"end":8.48,"confidence":0.999711,"punctuated_word":"say"},{"word":"it","start":8.48,"end":8.639999,"confidence":0.9998085,"punctuated_word":"it"},{"word":"again","start":8.639999,"end":9.139999,"confidence":0.95393705,"punctuated_word":"again."},{"word":"life","start":9.991312,"end":10.391313,"confidence":0.999337,"punctuated_word":"Life"},{"word":"moves","start":10.391313,"end":10.711312,"confidence":0.99979717,"punctuated_word":"moves"},{"word":"pretty","start":10.711312,"end":11.031313,"confidence":0.99983037,"punctuated_word":"pretty"},{"word":"fast","start":11.031313,"end":11.531313,"confidence":0.9997682,"punctuated_word":"fast."},{"word":"you","start":11.991312,"end":12.231313,"confidence":0.95979714,"punctuated_word":"You"},{"word":"don't","start":12.231313,"end":12.4713125,"confidence":0.9999173,"punctuated_word":"don't"},{"word":"stop","start":12.4713125,"end":12.711312,"confidence":0.9998498,"punctuated_word":"stop"},{"word":"and","start":12.711312,"end":12.871312,"confidence":0.99942267,"punctuated_word":"and"},{"word":"look","start":12.871312,"end":13.031313,"confidence":0.99988985,"punctuated_word":"look"},{"word":"around","start":13.031313,"end":13.351313,"confidence":0.999853,"punctuated_word":"around"},{"word":"once","start":13.351313,"end":13.591312,"confidence":0.99923956,"punctuated_word":"once"},{"word":"in","start":13.591312,"end":13.751312,"confidence":0.9984458,"punctuated_word":"in"},{"word":"a","start":13.751312,"end":13.831312,"confidence":0.98450977,"punctuated_word":"a"},{"word":"while","start":13.831312,"end":14.331312,"confidence":0.94350064,"punctuated_word":"while,"},{"word":"you","start":14.631312,"end":14.791312,"confidence":0.99865144,"punctuated_word":"you"},{"word":"could","start":14.791312,"end":14.951313,"confidence":0.99964106,"punctuated_word":"could"},{"word":"miss","start":14.951313,"end":15.191313,"confidence":0.9996731,"punctuated_word":"miss"},{"word":"it","start":15.191313,"end":15.691313,"confidence":0.9976532,"punctuated_word":"it."}]}]}],"summary":{"short":"Yep. I said it before, and I'll say it again. Life moves pretty fast. You don't stop and look around once in a while, you could miss it.","result":"success"}}}
1+
{"metadata":{"transaction_key":"deprecated","request_id":"014aadc7-9759-4524-aa53-5754b3d619b9","sha256":"5324da68ede209a16ac69a38e8cd29cee4d754434a041166cda3a1f5e0b24566","created":"2025-08-11T22:31:34.431Z","duration":17.566313,"channels":1,"models":["1abfe86b-e047-4eed-858a-35e5625b41ee"],"model_info":{"1abfe86b-e047-4eed-858a-35e5625b41ee":{"name":"2-general-nova","version":"2024-01-06.5664","arch":"nova-2"}},"summary_info":{"model_uuid":"67875a7f-c9c4-48a0-aa55-5bdb8a91c34a"}},"results":{"channels":[{"alternatives":[{"transcript":"Yep. I said it before, and I'll say it again. Life moves pretty fast. You don't stop and look around once in a while, you could miss it.","confidence":0.99953616,"words":[{"word":"yep","start":5.6,"end":6.1,"confidence":0.99766004,"punctuated_word":"Yep."},{"word":"i","start":7.04,"end":7.2799997,"confidence":0.7080212,"punctuated_word":"I"},{"word":"said","start":7.2799997,"end":7.44,"confidence":0.96501154,"punctuated_word":"said"},{"word":"it","start":7.44,"end":7.6,"confidence":0.99953616,"punctuated_word":"it"},{"word":"before","start":7.6,"end":7.9199996,"confidence":0.81644833,"punctuated_word":"before,"},{"word":"and","start":7.9199996,"end":8.08,"confidence":0.9998956,"punctuated_word":"and"},{"word":"i'll","start":8.08,"end":8.24,"confidence":0.99988514,"punctuated_word":"I'll"},{"word":"say","start":8.24,"end":8.48,"confidence":0.999711,"punctuated_word":"say"},{"word":"it","start":8.48,"end":8.639999,"confidence":0.9998085,"punctuated_word":"it"},{"word":"again","start":8.639999,"end":9.139999,"confidence":0.95393544,"punctuated_word":"again."},{"word":"life","start":9.991312,"end":10.391313,"confidence":0.999337,"punctuated_word":"Life"},{"word":"moves","start":10.391313,"end":10.711312,"confidence":0.99979717,"punctuated_word":"moves"},{"word":"pretty","start":10.711312,"end":11.031313,"confidence":0.99983037,"punctuated_word":"pretty"},{"word":"fast","start":11.031313,"end":11.531313,"confidence":0.9997682,"punctuated_word":"fast."},{"word":"you","start":11.991312,"end":12.231313,"confidence":0.9598434,"punctuated_word":"You"},{"word":"don't","start":12.231313,"end":12.4713125,"confidence":0.9999174,"punctuated_word":"don't"},{"word":"stop","start":12.4713125,"end":12.711312,"confidence":0.9998498,"punctuated_word":"stop"},{"word":"and","start":12.711312,"end":12.871312,"confidence":0.99942255,"punctuated_word":"and"},{"word":"look","start":12.871312,"end":13.031313,"confidence":0.99988985,"punctuated_word":"look"},{"word":"around","start":13.031313,"end":13.351313,"confidence":0.999853,"punctuated_word":"around"},{"word":"once","start":13.351313,"end":13.591312,"confidence":0.9992397,"punctuated_word":"once"},{"word":"in","start":13.591312,"end":13.751312,"confidence":0.99844617,"punctuated_word":"in"},{"word":"a","start":13.751312,"end":13.831312,"confidence":0.98451185,"punctuated_word":"a"},{"word":"while","start":13.831312,"end":14.331312,"confidence":0.94352007,"punctuated_word":"while,"},{"word":"you","start":14.631312,"end":14.791312,"confidence":0.99865156,"punctuated_word":"you"},{"word":"could","start":14.791312,"end":14.951313,"confidence":0.99964106,"punctuated_word":"could"},{"word":"miss","start":14.951313,"end":15.191313,"confidence":0.99967325,"punctuated_word":"miss"},{"word":"it","start":15.191313,"end":15.691313,"confidence":0.99765414,"punctuated_word":"it."}]}]}],"summary":{"short":"Yep. I said it before, and I'll say it again. Life moves pretty fast. You don't stop and look around once in a while, you could miss it.","result":"success"}}}

0 commit comments

Comments
 (0)