Skip to content

Commit 2e3fa20

Browse files
committed
fix(a2a): usar UUID do contato como userId (corrige 500 Session not found)
Segunda metade do fix de chaveamento de sessão (o commit anterior corrigiu o session_id via contextId; este corrige o user_id). A sessão ADK do Processor é chaveada por (app_name, user_id, session_id). O SessionSync do CRM pré-cria/persiste essa sessão usando o UUID do contato como user_id. Mas o bot_runtime enviava o ContactID numérico como userId, então o run a2a buscava a sessão com user_id numérico enquanto a linha persistida tinha o UUID do contato. Resultado: get_session_by_id (que ignora user_id) achava a sessão e logava o histórico, mas o runner do ADK (que exige user_id) levantava "Session not found" → 500 → o agente nunca respondia, mesmo com a sessão e o histórico existindo. Correção: extrair o UUID do contato de metadata.evoai_crm_data.contact.id e usá-lo como userId, com fallback para o ContactID numérico quando o metadata estiver ausente. Mesmo padrão de extração e fallback do fix do contextId. Isso alinha os três pontos: escrita do SessionSync, criação e leitura do runner. O caller Rails (http_request_service) já enviava o contact UUID (sender.id); este fix alinha o bot_runtime ao mesmo padrão. Testes: pkg/ai/service passa, cobrindo userId UUID e fallback numérico.
1 parent dfe6b2b commit 2e3fa20

2 files changed

Lines changed: 56 additions & 7 deletions

File tree

pkg/ai/service/ai_adapter.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,22 @@ func (a *aiAdapter) Call(ctx context.Context, req *model.A2ARequest) (*model.Nor
5757
// to the numeric ID only when the metadata is absent (e.g. legacy callers).
5858
contextID := conversationContextID(req.Metadata, req.ConversationID)
5959

60+
// userId MUST be the contact UUID, not the numeric ContactID. The Processor's
61+
// ADK session is keyed on (app_name, user_id, session_id). The CRM's SessionSync
62+
// pre-creates/persists that session with the contact UUID as user_id, so when the
63+
// a2a run looks it up with the numeric ContactID the keys diverge and the runner
64+
// raises "Session not found" → 500 → the agent never replies (even though the
65+
// session and its history exist). Prefer the contact UUID from the CRM metadata;
66+
// fall back to the numeric ID only when the metadata is absent (legacy callers).
67+
userID := contactUserID(req.Metadata, req.ContactID)
68+
6069
rpcReq := model.JSONRPCRequest{
6170
JSONRPC: "2.0",
6271
ID: fmt.Sprintf("%d:%d", req.ContactID, req.ConversationID),
6372
Method: "message/send",
6473
Params: model.JSONRPCParams{
6574
ContextID: contextID,
66-
UserID: fmt.Sprintf("%d", req.ContactID),
75+
UserID: userID,
6776
Message: model.JSONRPCMessage{
6877
Role: "user",
6978
Parts: []model.JSONRPCPart{
@@ -187,3 +196,32 @@ func extractConversationUUID(metadata map[string]any) string {
187196
}
188197
return id
189198
}
199+
200+
// contactUserID resolves the userId for the JSON-RPC call. It reads the contact
201+
// UUID the CRM nests at metadata.evoai_crm_data.contact.id (the same value the CRM's
202+
// SessionSync uses as the ADK session user_id) and returns it; if any hop is missing
203+
// or empty it falls back to the numeric contact ID (legacy behaviour).
204+
func contactUserID(metadata map[string]any, contactID int64) string {
205+
if uuid := extractContactUUID(metadata); uuid != "" {
206+
return uuid
207+
}
208+
return fmt.Sprintf("%d", contactID)
209+
}
210+
211+
// extractContactUUID digs metadata.evoai_crm_data.contact.id out of the untyped CRM
212+
// metadata map. Returns "" when the path is absent or not a string.
213+
func extractContactUUID(metadata map[string]any) string {
214+
crmData, ok := metadata["evoai_crm_data"].(map[string]any)
215+
if !ok {
216+
return ""
217+
}
218+
contact, ok := crmData["contact"].(map[string]any)
219+
if !ok {
220+
return ""
221+
}
222+
id, ok := contact["id"].(string)
223+
if !ok {
224+
return ""
225+
}
226+
return id
227+
}

pkg/ai/service/ai_adapter_test.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ import (
1616
)
1717

1818
// crmMetadata builds the nested CRM metadata map the Rails AgentBotListener sends,
19-
// carrying the conversation UUID at evoai_crm_data.conversation.id.
20-
func crmMetadata(conversationUUID string) map[string]any {
19+
// carrying the conversation UUID at evoai_crm_data.conversation.id and the contact
20+
// UUID at evoai_crm_data.contact.id.
21+
func crmMetadata(conversationUUID, contactUUID string) map[string]any {
2122
return map[string]any{
2223
"evoai_crm_data": map[string]any{
2324
"conversation": map[string]any{
2425
"id": conversationUUID,
2526
"display_id": 7,
2627
},
28+
"contact": map[string]any{
29+
"id": contactUUID,
30+
"name": "Davidson Gomes",
31+
},
2732
},
2833
}
2934
}
@@ -58,8 +63,9 @@ func TestCall_Success(t *testing.T) {
5863
if req.Params.ContextID != "conv-uuid-abc" {
5964
t.Errorf("req.Params.ContextID = %q, want %q", req.Params.ContextID, "conv-uuid-abc")
6065
}
61-
if req.Params.UserID != "42" {
62-
t.Errorf("req.Params.UserID = %q, want %q", req.Params.UserID, "42")
66+
// userId must be the contact UUID from metadata, not the numeric ContactID.
67+
if req.Params.UserID != "contact-uuid-xyz" {
68+
t.Errorf("req.Params.UserID = %q, want %q", req.Params.UserID, "contact-uuid-xyz")
6369
}
6470
if len(req.Params.Message.Parts) != 1 || req.Params.Message.Parts[0].Text != "hello world" {
6571
t.Errorf("message parts = %+v, want single part with text 'hello world'", req.Params.Message.Parts)
@@ -86,7 +92,7 @@ func TestCall_Success(t *testing.T) {
8692
ContactID: 42,
8793
ConversationID: 7,
8894
ApiKey: "test-key",
89-
Metadata: crmMetadata("conv-uuid-abc"),
95+
Metadata: crmMetadata("conv-uuid-abc", "contact-uuid-xyz"),
9096
})
9197
if err != nil {
9298
t.Fatalf("Call returned unexpected error: %v", err)
@@ -108,6 +114,10 @@ func TestCall_ContextID_FallsBackToNumericID(t *testing.T) {
108114
if req.Params.ContextID != "7" {
109115
t.Errorf("req.Params.ContextID = %q, want fallback %q", req.Params.ContextID, "7")
110116
}
117+
// Without metadata, userId falls back to the numeric ContactID.
118+
if req.Params.UserID != "42" {
119+
t.Errorf("req.Params.UserID = %q, want fallback %q", req.Params.UserID, "42")
120+
}
111121
json.NewEncoder(w).Encode(aiModel.A2AResponse{
112122
Result: &aiModel.A2AResult{
113123
Artifacts: []aiModel.A2AArtifact{
@@ -122,9 +132,10 @@ func TestCall_ContextID_FallsBackToNumericID(t *testing.T) {
122132
_, err := adapter.Call(context.Background(), &aiModel.A2ARequest{
123133
OutgoingURL: server.URL,
124134
Message: "test",
135+
ContactID: 42,
125136
ConversationID: 7,
126137
ApiKey: "key",
127-
// No Metadata → must fall back to the numeric ConversationID.
138+
// No Metadata → must fall back to the numeric ConversationID/ContactID.
128139
})
129140
if err != nil {
130141
t.Fatalf("Call returned unexpected error: %v", err)

0 commit comments

Comments
 (0)