Skip to content

feat: add database tables and API routes for agentic chat feature #17570

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

Merged
merged 18 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve chat
  • Loading branch information
kylecarbs authored and johnstcn committed May 2, 2025
commit 9068d8b271c8d4c2eb2e8b31fc70312b27c041d8
14 changes: 9 additions & 5 deletions coderd/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,13 @@ func (api *API) postChatMessages(w http.ResponseWriter, r *http.Request) {
return
}

deps := toolsdk.Deps{
CoderClient: client,
deps, err := toolsdk.NewDeps(client)
if err != nil {
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to create tool dependencies",
Detail: err.Error(),
})
return
}

for {
Expand All @@ -254,10 +259,9 @@ func (api *API) postChatMessages(w http.ResponseWriter, r *http.Request) {
Model: req.Model,
Messages: messages,
Tools: tools,
SystemPrompt: `You are a chat assistant for Coder. You will attempt to resolve the user's
request to the maximum utilization of your tools.
SystemPrompt: `You are a chat assistant for Coder - an open-source platform for creating and managing cloud development environments on any infrastructure. You are expected to be precise, concise, and helpful.

Try your best to not ask the user for help - solve the task with your tools!`,
You are running as an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Do NOT guess or make up an answer.`,
})
if err != nil {
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
Expand Down
9 changes: 8 additions & 1 deletion codersdk/toolsdk/toolsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ This resource provides the following fields:
- init_script: The script to run on provisioned infrastructure to fetch and start the agent.
- token: Set the environment variable CODER_AGENT_TOKEN to this value to authenticate the agent.

The agent MUST be installed and started using the init_script.
The agent MUST be installed and started using the init_script. A utility like curl or wget to fetch the agent binary must exist in the provisioned infrastructure.

Expose terminal or HTTP applications running in a workspace with:

Expand Down Expand Up @@ -711,13 +711,20 @@ resource "google_compute_instance" "dev" {
auto_delete = false
source = google_compute_disk.root.name
}
// In order to use google-instance-identity, a service account *must* be provided.
service_account {
email = data.google_compute_default_service_account.default.email
scopes = ["cloud-platform"]
}
# ONLY FOR WINDOWS:
# metadata = {
# windows-startup-script-ps1 = coder_agent.main.init_script
# }
# The startup script runs as root with no $HOME environment set up, so instead of directly
# running the agent init script, create a user (with a homedir, default shell and sudo
# permissions) and execute the init script as that user.
#
# The agent MUST be started in here.
metadata_startup_script = <<EOMETA
#!/usr/bin/env sh
set -eux
Expand Down
86 changes: 38 additions & 48 deletions site/src/pages/ChatPage/ChatMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Paper, { PaperProps } from "@mui/material/Paper";
import TextField from "@mui/material/TextField";
import { getChatMessages, getChats } from "api/queries/chats";
import { CreateChatMessageRequest, ChatMessage } from "api/typesGenerated";
import { FC, memo, useEffect, useRef, KeyboardEvent } from "react";
import { FC, memo, useEffect, useRef, KeyboardEvent, useCallback } from "react";
import { useQuery, useQueryClient } from "react-query";
import { useLocation, useParams } from "react-router-dom";
import { ChatLandingLocationState } from "./ChatLanding";
Expand Down Expand Up @@ -291,7 +291,7 @@ interface ChatViewProps {
messages: Message[];
input: string;
handleInputChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement>;
hhandleSubmit: (e?: React.FormEvent<HTMLFormElement>) => void;
handleSubmit: (e?: React.FormEvent<HTMLFormElement>) => void;
isLoading: boolean;
chatID: string;
}
Expand All @@ -300,7 +300,7 @@ const ChatView: FC<ChatViewProps> = ({
messages,
input,
handleInputChange,
hhandleSubmit,
handleSubmit,
isLoading,
chatID
}) => {
Expand All @@ -322,7 +322,7 @@ const ChatView: FC<ChatViewProps> = ({
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
hhandleSubmit();
handleSubmit();
}
};

Expand Down Expand Up @@ -355,34 +355,6 @@ const ChatView: FC<ChatViewProps> = ({
{messages.map((message, index) => (
<MessageBubble key={`message-${index}`} message={message} />
))}
{isLoading && (
<div
css={{
display: "flex",
justifyContent: "flex-start",
maxWidth: "80%",
animation: `${fadeIn} 0.3s ease-out`,
}}
>
<Paper
elevation={1}
css={{
padding: theme.spacing(1.5, 2),
fontSize: "0.95rem",
backgroundColor: theme.palette.background.paper,
borderRadius: "16px",
borderBottomLeftRadius: "4px",
width: "auto",
display: "flex",
alignItems: "center",
gap: theme.spacing(1),
animation: `${pulseAnimation} 1.5s ease-in-out infinite`,
}}
>
<Loader size={20} /> Thinking...
</Paper>
</div>
)}
<div ref={messagesEndRef} />
</div>
</div>
Expand All @@ -400,7 +372,7 @@ const ChatView: FC<ChatViewProps> = ({
>
<Paper
component="form"
onSubmit={hhandleSubmit}
onSubmit={handleSubmit}
elevation={0}
variant="outlined"
css={{
Expand Down Expand Up @@ -481,10 +453,11 @@ export const ChatMessages: FC = () => {
const {
messages,
input,
handleInputChange,
handleInputChange: originalHandleInputChange,
handleSubmit: originalHandleSubmit,
isLoading,
setInput,
setInput,
setMessages,
} = useChat({
id: chatID,
api: `/api/v2/chats/${chatID}/messages`,
Expand All @@ -502,22 +475,39 @@ export const ChatMessages: FC = () => {
initialInput: transferedState?.message,
initialMessages: messagesQuery.data as Message[] | undefined,
});
useEffect(() => {
// console.log(transferedState?.message, input)
if (transferedState?.message && input === transferedState?.message) {
// handleSubmit();
}
}, [transferedState?.message])

const handleSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
if (e) e.preventDefault();
if (!input.trim()) return;
originalHandleSubmit();
setInput('');
};
// Update messages from query data when it loads
useEffect(() => {
if (messagesQuery.data && messages.length === 0) {
setMessages(messagesQuery.data as Message[]);
}
}, [messagesQuery.data, messages.length, setMessages]);

// Wrap handlers in useCallback
const handleInputChange = useCallback(originalHandleInputChange, [originalHandleInputChange]);

const handleSubmitCallback = useCallback((e?: React.FormEvent<HTMLFormElement>) => {
if (e) e.preventDefault();
if (!input.trim()) return;
originalHandleSubmit();
setInput(''); // Clear input after submit
}, [input, originalHandleSubmit, setInput]);

// Clear input and potentially submit on initial load with message
useEffect(() => {
if (transferedState?.message && input === transferedState.message) {
// Prevent submitting if messages already exist (e.g., browser back/forward)
if (messages.length === (messagesQuery.data?.length ?? 0)) {
handleSubmitCallback(); // Use the correct callback name
}
// Clear the state to prevent re-submission on subsequent renders/navigation
window.history.replaceState({}, document.title);
}
}, [transferedState?.message, input, handleSubmitCallback, messages.length, messagesQuery.data?.length]); // Use the correct callback name

useEffect(() => {
if (transferedState?.message) {
// Logic potentially related to transferedState can go here if needed,
}
}, [transferedState?.message]);

Expand All @@ -536,7 +526,7 @@ export const ChatMessages: FC = () => {
messages={messages}
input={input}
handleInputChange={handleInputChange}
hhandleSubmit={handleSubmit}
handleSubmit={handleSubmitCallback}
isLoading={isLoading}
/>
);
Expand Down
Loading