-
Notifications
You must be signed in to change notification settings - Fork 159
[PLAT-333] share/fork conversation support #8538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
proto/rill/runtime/v1/api.proto
Outdated
| message ShareConversationRequest { | ||
| string instance_id = 1 [(validate.rules).string = {pattern: "^[_\\-a-zA-Z0-9]+$"}]; | ||
| string conversation_id = 2 [(validate.rules).string.min_len = 1]; | ||
| string shared_until_message_id = 3; // optional message ID up to which to share otherwise share all current messages, only valid conversation having last message of "result" type from "router" agent till until this message ID will be shared |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- nit: can avoid repeating type name in field name, i.e. call the API field
ShareConversationRequest.until_message_id - Is there an easy way to support un-sharing? Like a bool flag or supporting "none" as a constant value for
shared_until_message_id?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Sure
- Yeah we can support some constant to un-share or a flag.
| // Load the session in the catalog | ||
| session, err := catalog.FindAISession(ctx, opts.SessionID) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to find session %q: %w", opts.SessionID, err) | ||
| } | ||
|
|
||
| // Check access: for now, only allow users to access their own sessions or shared sessions with trimmed messages. | ||
| // Checking !SkipChecks to ensure access for superusers and for Rill Developer (where auth is disabled and SkipChecks is true). | ||
| if opts.Claims.UserID != session.OwnerID && !opts.Claims.SkipChecks && session.SharedUntilMessageID == "" { | ||
| return "", fmt.Errorf("access denied to session %q", session.ID) | ||
| } | ||
|
|
||
| retrieveUntilMessageID := session.SharedUntilMessageID | ||
| if session.OwnerID == opts.Claims.UserID || opts.Claims.SkipChecks { | ||
| // If the user owns the session or skipCheck enabled, they can see all messages. | ||
| retrieveUntilMessageID = "" | ||
| } | ||
|
|
||
| var messages []*Message | ||
| ms, err := catalog.FindAIMessages(ctx, opts.SessionID) | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to find messages for session %q: %w", opts.SessionID, err) | ||
| } | ||
| for _, m := range ms { | ||
| messages = append(messages, &Message{ | ||
| ID: m.ID, | ||
| ParentID: m.ParentID, | ||
| SessionID: m.SessionID, | ||
| Time: m.CreatedOn, | ||
| Index: m.Index, | ||
| Role: Role(m.Role), | ||
| Type: MessageType(m.Type), | ||
| Tool: m.Tool, | ||
| ContentType: MessageContentType(m.ContentType), | ||
| Content: m.Content, | ||
| }) | ||
| // only load messages up to and including that retrieveUntilMessageID; messages are ordered by "Index" ascending. | ||
| if m.ID == retrieveUntilMessageID { | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this the same logic as in r.Session()? Can it use r.Session(...).Messages() instead to get the messages?
Or what do you think about making it a on Session, like s.ai.Session(...).Fork(newOwner)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a Session does a lot more than what is needed here, like setting up the logger, creating activity client etc. and may add more things in future. Thus it seems unnecessary, mainly we want messages with some access checks so not much logic there anyways.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Session() is actually meant to be the single entrypoint for accessing a session, to make sure we parse DB messages and enforce access checks uniformly. So it would be nice to avoid the duplication.
The logger/activity clients are cheap, so no need to worry about those. And if we need to add other non-cheap logic in the future, we can do it in a lazy-loading way to make sure Session remains useful as the single entrypoint for all use cases.
runtime/server/chat.go
Outdated
| var anonUser bool | ||
| if claims != nil && claims.UserID == "" && !claims.SkipChecks { | ||
| anonUser = true | ||
| } | ||
| if !claims.Can(runtime.UseAI) && !anonUser { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should still require project access to see shared conversations.
And otherwise, it's not safe to handle anon users like this – we'd have to break UseAI up into separate ReadAI and WriteAI permissions and handle it using auth.OwnerTypeAnon around here:
Line 304 in e2db39a
| if claims.OwnerType() == auth.OwnerTypeUser { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I thought any shared conversation can be viewable just using link without logging like ChatGPT. Anyways, removed anon access.
runtime/server/chat.go
Outdated
| if anonUser && !session.Shareable() { | ||
| return nil, ErrForbidden | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't access be checked inside s.ai.Session? Actually, it looks like it's already checked there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes its already checked inside and allows either owner or if its shared then trims messages till shared id.
| // validate that the requested message id exists | ||
| foundMsg := req.SharedUntilMessageId == "" | ||
| lastRouterResultMsgID := "" | ||
| for _, msg := range session.Messages() { | ||
| // only allow sharing until the last router agent result message | ||
| if msg.Tool == ai.RouterAgentName && msg.Type == ai.MessageTypeResult { | ||
| lastRouterResultMsgID = msg.ID | ||
| } | ||
| if msg.ID == req.SharedUntilMessageId { | ||
| // found the message, proceed | ||
| foundMsg = true | ||
| break | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The session has predicate support for searching messages, which it would be nice to use, for example:
var preds []ai.Predicate
if req.SharedUntilMessageId == "" {
preds = []ai.Predicate{ai.FilterByTool(ai.RouterAgentName), ai.FilterByType(ai.MessageTypeResult)}
} else {
preds = []ai.Predicate{ai.FilterByID(req.SharedUntilMessageId)}
}
msg, ok := s.LatestMessage(preds)
...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I saw that but for the case when req.SharedUntilMessageId != "" and the SharedUntilMessageId is not a router result then I am looking for latest router result before this message and using that. Since that will require manual looping anyways so I just clubbed both the cases together in single loop lookup. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since that will require manual looping anyways
I'm not sure I follow manual looping is required anyway? I think it should be possible to remove manual looping completely.
Are you just thinking about the error message? It can just be a generic error like "shared_until_message_id does not match a router_agent result message" (doesn't need to handle not found vs. wrong type explicitly)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No I mean when req.SharedUntilMessageId != "" and if this message is not a router result then we will need to loop through messages to find latest router result before this message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's okay to just error if shared_until_message_id does not point to a valid router result message. (The UI can easily ensure this.)
Sessions can also have other root calls than router calls (like MCP messages and soon feedback messages), so too much inference can anyway open up new edge cases.
begelundmuller
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pjain1 I also responded with some suggestions on the unresolved comments above :)
| {{ .fork_instructions }} | ||
| This conversation has been transferred and the new owner may have different access permissions. | ||
| If you start seeing access errors like "action not allowed"", "resource not found" (for resources earlier available) etc., consider repeating metadata listings and lookups. | ||
| If you run into these issues, explicitly mention to user that this may be due to conversation forking and user may not have access to metrics view that previous user had. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| If you run into these issues, explicitly mention to user that this may be due to conversation forking and user may not have access to metrics view that previous user had. | |
| If you run into such issues, explicitly mention to the user that this may be due to conversation forking and that they may not have access to the data that the previous user had. |
Adds supports for sharing and forking shared conversations. Once a conversation is shared, anyone knowing the conversation id can access till the shared message. User can then start a new conversation from it which copies over the shared messages to new conversation.
Now after forking new user might not have same access as previous in which case they might get access errors on continuing conversation, we let LLM know about this by injecting this info in the system prompt for shared conversations. On testing I have seen replies like this from LLM
Unhandled case - However, there is an edge case around chart message. Existing charts does MV agg API calls which will fail if the new user does not have access to the underlying mv. So charts will just show spinner.
New APIs
ShareConversation, optionally acceptsshared_until_message_id, returns nothing: Only owners (or rill dev) may call. Validates the provided message exists in the caller’s view. Persistsshared_until_message_idas the last router result at or before the requested message; if omitted, uses the latest router result.ForkConversation, accepts nothing and returns newconversation_id: Clones the conversation; owners get all messages, non-owners only get messages up to the shared boundary.Checklist: