Skip to content

Commit af38e26

Browse files
peterjEItanya
andauthored
initial feedback feature (#427)
* wip Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> * wip Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> * first stage of feedback feature (ability to provide feedback and store it) Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> * lint Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> * pr feedback Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> --------- Signed-off-by: Peter Jausovec <peter.jausovec@solo.io> Co-authored-by: Eitan Yarmush <eitan.yarmush@solo.io>
1 parent c8806b4 commit af38e26

16 files changed

Lines changed: 546 additions & 23 deletions

File tree

go/autogen/client/feedback.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package client
2+
3+
import "fmt"
4+
5+
func (c *Client) CreateFeedback(feedback *FeedbackSubmission) error {
6+
err := c.doRequest("POST", "/feedback/", feedback, nil)
7+
if err != nil {
8+
return err
9+
}
10+
11+
return nil
12+
}
13+
14+
func (c *Client) ListFeedback(userID string) ([]*FeedbackSubmission, error) {
15+
var response []*FeedbackSubmission
16+
err := c.doRequest("GET", fmt.Sprintf("/feedback/?user_id=%s", userID), nil, &response)
17+
if err != nil {
18+
return nil, err
19+
}
20+
21+
return response, nil
22+
}

go/autogen/client/types.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,37 @@ func streamSseResponse(r io.ReadCloser) chan *SseEvent {
187187
}()
188188
return ch
189189
}
190+
191+
// FeedbackIssueType represents the category of feedback issue
192+
type FeedbackIssueType string
193+
194+
const (
195+
FeedbackIssueTypeInstructions FeedbackIssueType = "instructions" // Did not follow instructions
196+
FeedbackIssueTypeFactual FeedbackIssueType = "factual" // Not factually correct
197+
FeedbackIssueTypeIncomplete FeedbackIssueType = "incomplete" // Incomplete response
198+
FeedbackIssueTypeTool FeedbackIssueType = "tool" // Should have run the tool
199+
)
200+
201+
// FeedbackSubmission defines the request payload for submitting feedback
202+
// and also serves as the response object when listing feedback.
203+
type FeedbackSubmission struct {
204+
// ID is the unique identifier for the feedback, present in responses.
205+
ID int `json:"id,omitempty"`
206+
// CreatedAt is the timestamp of feedback creation, present in responses.
207+
CreatedAt string `json:"created_at,omitempty"`
208+
// UpdatedAt is the timestamp of the last update, present in responses.
209+
UpdatedAt string `json:"updated_at,omitempty"`
210+
// Version of the feedback object, present in responses.
211+
Version string `json:"version,omitempty"`
212+
213+
// UserID is the identifier for the user submitting the feedback.
214+
UserID string `json:"user_id,omitempty"`
215+
// IsPositive indicates if the feedback is positive. Required for request.
216+
IsPositive bool `json:"is_positive"`
217+
// FeedbackText is the textual content of the feedback. Required for request.
218+
FeedbackText string `json:"feedback_text"`
219+
// IssueType categorizes the feedback if it's negative. Optional.
220+
IssueType *FeedbackIssueType `json:"issue_type,omitempty"`
221+
// MessageID is the ID of the message this feedback pertains to.
222+
MessageID int `json:"message_id,omitempty"`
223+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package handlers
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"net/http"
7+
8+
"github.com/kagent-dev/kagent/go/autogen/client"
9+
"github.com/kagent-dev/kagent/go/controller/internal/httpserver/errors"
10+
ctrllog "sigs.k8s.io/controller-runtime/pkg/log"
11+
)
12+
13+
// FeedbackHandler handles user feedback submissions
14+
type FeedbackHandler struct {
15+
*Base
16+
}
17+
18+
// NewFeedbackHandler creates a new feedback handler
19+
func NewFeedbackHandler(base *Base) *FeedbackHandler {
20+
return &FeedbackHandler{Base: base}
21+
}
22+
23+
// HandleCreateFeedback handles the submission of user feedback and forwards it to the Python backend
24+
func (h *FeedbackHandler) HandleCreateFeedback(w ErrorResponseWriter, r *http.Request) {
25+
log := ctrllog.FromContext(r.Context()).WithName("feedback-handler").WithValues("operation", "create-feedback")
26+
27+
log.Info("Received feedback submission")
28+
29+
// Read request body
30+
body, err := io.ReadAll(r.Body)
31+
if err != nil {
32+
log.Error(err, "Failed to read request body")
33+
w.RespondWithError(errors.NewBadRequestError("Failed to read request body", err))
34+
return
35+
}
36+
37+
// Parse the feedback submission request
38+
var feedbackReq client.FeedbackSubmission
39+
if err := json.Unmarshal(body, &feedbackReq); err != nil {
40+
log.Error(err, "Failed to parse feedback data")
41+
w.RespondWithError(errors.NewBadRequestError("Invalid feedback data format", err))
42+
return
43+
}
44+
45+
// Validate the request
46+
if feedbackReq.FeedbackText == "" {
47+
log.Error(nil, "Missing required field: feedbackText")
48+
w.RespondWithError(errors.NewBadRequestError("Missing required field: feedbackText", nil))
49+
return
50+
}
51+
52+
err = h.AutogenClient.CreateFeedback(&feedbackReq)
53+
if err != nil {
54+
log.Error(err, "Failed to create feedback")
55+
w.RespondWithError(errors.NewInternalServerError("Failed to create feedback", err))
56+
return
57+
}
58+
59+
log.Info("Feedback successfully submitted")
60+
RespondWithJSON(w, http.StatusOK, "Feedback submitted successfully")
61+
}
62+
63+
func (h *FeedbackHandler) HandleListFeedback(w ErrorResponseWriter, r *http.Request) {
64+
log := ctrllog.FromContext(r.Context()).WithName("feedback-handler").WithValues("operation", "list-feedback")
65+
66+
log.Info("Listing feedback")
67+
68+
userID, err := GetUserID(r)
69+
if err != nil {
70+
log.Error(err, "Failed to get user ID")
71+
w.RespondWithError(errors.NewBadRequestError("Failed to get user ID", err))
72+
return
73+
}
74+
75+
feedback, err := h.AutogenClient.ListFeedback(userID)
76+
if err != nil {
77+
log.Error(err, "Failed to list feedback")
78+
w.RespondWithError(errors.NewInternalServerError("Failed to list feedback", err))
79+
return
80+
}
81+
82+
log.Info("Feedback listed successfully")
83+
RespondWithJSON(w, http.StatusOK, feedback)
84+
}

go/controller/internal/httpserver/handlers/handlers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type Handlers struct {
1919
ToolServers *ToolServersHandler
2020
Invoke *InvokeHandler
2121
Memory *MemoryHandler
22+
Feedback *FeedbackHandler
2223
}
2324

2425
// Base holds common dependencies for all handlers
@@ -47,5 +48,6 @@ func NewHandlers(kubeClient client.Client, autogenClient *autogen_client.Client,
4748
ToolServers: NewToolServersHandler(base),
4849
Invoke: NewInvokeHandler(base),
4950
Memory: NewMemoryHandler(base),
51+
Feedback: NewFeedbackHandler(base),
5052
}
5153
}

go/controller/internal/httpserver/handlers/sessions.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,16 @@ func (h *SessionsHandler) HandleListSessionMessages(w ErrorResponseWriter, r *ht
200200
configs := []autogen_client.TaskMessageMap{}
201201
for _, run := range runs {
202202
for _, message := range run.Messages {
203-
configs = append(configs, message.Config)
203+
item := make(autogen_client.TaskMessageMap)
204+
if message.Config != nil {
205+
for k, v := range message.Config {
206+
item[k] = v
207+
}
208+
}
209+
item["id"] = message.ID
210+
configs = append(configs, item)
204211
}
205212
}
206-
207-
log.Info("Successfully listed session runs", "count", len(runs))
208213
RespondWithJSON(w, http.StatusOK, configs)
209214
}
210215

go/controller/internal/httpserver/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
APIPathModels = "/api/models"
3030
APIPathMemories = "/api/memories"
3131
APIPathA2A = "/api/a2a"
32+
APIPathFeedback = "/api/feedback"
3233
)
3334

3435
var defaultModelConfig = types.NamespacedName{
@@ -165,6 +166,10 @@ func (s *HTTPServer) setupRoutes() {
165166
s.router.HandleFunc(APIPathMemories+"/{memoryName}", adaptHandler(s.handlers.Memory.HandleGetMemory)).Methods(http.MethodGet)
166167
s.router.HandleFunc(APIPathMemories+"/{memoryName}", adaptHandler(s.handlers.Memory.HandleUpdateMemory)).Methods(http.MethodPut)
167168

169+
// Feedback
170+
s.router.HandleFunc(APIPathFeedback, adaptHandler(s.handlers.Feedback.HandleCreateFeedback)).Methods(http.MethodPost)
171+
s.router.HandleFunc(APIPathFeedback, adaptHandler(s.handlers.Feedback.HandleListFeedback)).Methods(http.MethodGet)
172+
168173
// A2A
169174
s.router.PathPrefix(APIPathA2A).Handler(s.config.A2AHandler)
170175

python/src/autogenstudio/datamodel/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .db import BaseDBModel, Gallery, Message, Run, RunStatus, Session, Settings, Team, Tool, ToolServer
1+
from .db import BaseDBModel, Feedback, Gallery, Message, Run, RunStatus, Session, Settings, Team, Tool, ToolServer
22
from .types import (
33
EnvironmentVariable,
44
GalleryComponents,
@@ -35,4 +35,5 @@
3535
"EnvironmentVariable",
3636
"Gallery",
3737
"ToolServer",
38+
"Feedback",
3839
]

python/src/autogenstudio/datamodel/db.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from autogen_core import ComponentModel
88
from pydantic import ConfigDict, SecretStr, field_validator
99
from sqlalchemy import ForeignKey, Integer
10-
from sqlmodel import JSON, Column, DateTime, Field, SQLModel, func
10+
from sqlmodel import JSON, Column, DateTime, Field, Relationship, SQLModel, func
1111

1212
from .eval import EvalJudgeCriteria, EvalRunResult, EvalRunStatus, EvalScore, EvalTask
1313
from .types import (
@@ -65,6 +65,25 @@ class Message(BaseDBModel, table=True):
6565
run_id: Optional[int] = Field(default=None, sa_column=Column(Integer, ForeignKey("run.id", ondelete="CASCADE")))
6666

6767
message_meta: Optional[Union[MessageMeta, dict]] = Field(default={}, sa_column=Column(JSON))
68+
feedback: List["Feedback"] = Relationship(back_populates="message")
69+
70+
71+
class Feedback(BaseDBModel, table=True):
72+
"""
73+
Database model for storing user feedback on agent responses.
74+
"""
75+
76+
__table_args__ = {"sqlite_autoincrement": True}
77+
78+
is_positive: bool = Field(default=False, description="Whether the feedback is positive or negative")
79+
feedback_text: str = Field(description="The feedback text provided by the user")
80+
issue_type: Optional[str] = Field(default=None, description="Category of issue for negative feedback")
81+
82+
message_id: Optional[int] = Field(
83+
default=None, sa_column=Column(Integer, ForeignKey("message.id", ondelete="CASCADE"))
84+
)
85+
86+
message: Optional["Message"] = Relationship(back_populates="feedback")
6887

6988

7089
class Session(BaseDBModel, table=True):

python/src/autogenstudio/sessionmanager/sessionmanager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ async def start_stream(
169169

170170
# The messages[0].content isn't properly being serialized, so it
171171
# doesn't even get sent back to the client. (That's why we're seeing undefined in the UI)
172-
# I am using the stop_reason to send the error message back and specifically checking
172+
# I am using the stop_reason to send the error message back and specifically checking
173173
# for the message type (error).
174174
error_result = TeamResult(
175175
task_result=TaskResult(

python/src/autogenstudio/web/app.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,20 @@
1515
from .config import settings
1616
from .deps import cleanup_managers, init_auth_manager, init_managers, register_auth_dependencies
1717
from .initialization import AppInitializer
18-
from .routes import gallery, invoke, models, runs, sessions, settingsroute, teams, tool_servers, tools, validation, ws
18+
from .routes import (
19+
feedback,
20+
gallery,
21+
invoke,
22+
models,
23+
runs,
24+
sessions,
25+
settingsroute,
26+
teams,
27+
tool_servers,
28+
tools,
29+
validation,
30+
ws,
31+
)
1932

2033
# Initialize application
2134
app_file_path = os.path.dirname(os.path.abspath(__file__))
@@ -25,7 +38,6 @@
2538
@asynccontextmanager
2639
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
2740
"""
28-
Lifecycle manager for the FastAPI application.
2941
Handles initialization and cleanup of application resources.
3042
"""
3143

@@ -105,14 +117,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
105117
responses={404: {"description": "Not found"}},
106118
)
107119

108-
109-
api.include_router(
110-
ws.router,
111-
prefix="/ws",
112-
tags=["websocket"],
113-
responses={404: {"description": "Not found"}},
114-
)
115-
116120
api.include_router(
117121
validation.router,
118122
prefix="/validate",
@@ -169,6 +173,13 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
169173
responses={404: {"description": "Not found"}},
170174
)
171175

176+
api.include_router(
177+
feedback.router,
178+
prefix="/feedback",
179+
tags=["feedback"],
180+
responses={404: {"description": "Not found"}},
181+
)
182+
172183
# Version endpoint
173184

174185

0 commit comments

Comments
 (0)