Skip to content
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

Work on generating in-context ai responses in the assistant #8

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions _build/elements/plugins/aikit.plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
const leftbarTrigger = document.getElementById('modx-leftbar-trigger');
leftbarTrigger.parentNode.insertBefore(assistentElement, leftbarTrigger);

const assistant = new AIKit();
assistant.initialize(assistentElement, {
MODx.AIKit = new AIKit();
MODx.AIKit.initialize(assistentElement, {
assetsUrl: '$assetsUrl',
showSystemPrompt: $showSystemPrompt,
})
Expand Down
112 changes: 83 additions & 29 deletions assets/components/aikit/mgr/aikit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class AIKit {
this.rootElement = null;
this.assistantOpen = false;
this.currentConversation = null;
this.callback = null;
this.config = {
assetsUrl: '/assets/components/aikit/',
};
Expand All @@ -17,6 +18,21 @@ class AIKit {
this.config = {...this.config, ...config};
}

openWithContext(prompt, callback)
{
this.newPrompt = prompt;
this.callback = callback;
this.currentConversation = null;
if (this.messageRenderer) {
this.messageRenderer.reset();
}
if (this.assistantOpen) {
this.toggleAssistant();
}
this.toggleAssistant();
this.sendMessage("Perform in-context action.");
}

// Render the button to open the assistant
renderAssistantButton()
{
Expand Down Expand Up @@ -67,7 +83,6 @@ class AIKit {
this.messageContainer = document.createElement('div');
this.messageContainer.className = 'ai-assistant-message-container';
mainContent.appendChild(this.messageContainer);
// @todo showPrompt
this.messageRenderer = new MessageRenderer(this.messageContainer, this.config);

// Create the chat list container
Expand Down Expand Up @@ -104,6 +119,8 @@ class AIKit {
this.style.height = `${this.scrollHeight + 2}px`; // Adjust to content's height
});

this.inputarea = textarea;

// Container for send button and settings link
const buttonContainer = document.createElement('div');
buttonContainer.className = 'ai-assistant-button-container';
Expand All @@ -113,7 +130,10 @@ class AIKit {
sendButton.className = 'ai-assistant-send-button'; // Add class for custom styling
sendButton.innerHTML = '<i class="icon icon-paper-plane"></i>'; // Font Awesome send icon

sendButton.addEventListener('click', () => this.sendMessage(textarea, this.messageContainer));
sendButton.addEventListener('click', () => {
this.sendMessage(textarea.value.trim());
textarea.value = '';
});

// Settings link/icon below the send button
const settingsLink = document.createElement('a');
Expand Down Expand Up @@ -212,22 +232,25 @@ class AIKit {
}

// Send a message
sendMessage(textarea, messageContainer)
sendMessage(prompt)
{
if (!this.currentConversation) {
fetch(this.config.assetsUrl + 'api.php?a=/conversations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
body: JSON.stringify({
prompt: this.newPrompt || '',
}),
})
.then(async response => {
const jsonResponse = await response.json();
if (response.status === 201) {
this.currentConversation = jsonResponse.data.id; // Store the new conversation ID
this.sendMessage(textarea, messageContainer);
this.sendMessage(prompt);
this.fetchConversations();
this.newPrompt = null;
} else {
console.error('Error creating a new conversation:', jsonResponse.error || 'Unknown error');
}
Expand All @@ -239,24 +262,22 @@ class AIKit {
}

this.isLoading = true;
const messageContent = textarea.value.trim();
if (!messageContent) {
if (!prompt || prompt.length === 0) {
return;
}

const loadingIndicator = document.createElement('div');
loadingIndicator.textContent = 'Processing...';
messageContainer.appendChild(loadingIndicator);
this.messageContainer.appendChild(loadingIndicator);

this.awaitAsyncMessages(this.currentConversation);

textarea.value = '';
fetch(this.config.assetsUrl + 'api.php?a=/messages&conversation=' + this.currentConversation, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({content: messageContent}),
body: JSON.stringify({content: prompt}),
})
.then(async response => {
const jsonResponse = await response.json();
Expand All @@ -278,8 +299,7 @@ class AIKit {

awaitAsyncMessages(conversationId)
{
const lastMessageId = this.messageRenderer.renderedMessages.size > 0 ?
[...this.messageRenderer.renderedMessages.keys()].pop() : 0;
const lastMessageId = this.messageRenderer.lastMessageId;

fetch(`${this.config.assetsUrl}api.php?a=/conversation/await&conversation=${this.currentConversation}&last_message=${lastMessageId}`)
.then(response => response.json())
Expand Down Expand Up @@ -319,6 +339,9 @@ class MessageRenderer {
if (!this.config.showSystemPrompt && user_role === 'developer') {
return;
}
if (!this.lastMessageId || id > this.lastMessageId) {
this.lastMessageId = id;
}

// Check if the message is already rendered and hasn't changed
const existingMessageEl = this.renderedMessages.get(id);
Expand Down Expand Up @@ -372,14 +395,54 @@ class MessageRenderer {
</div>
`;
} else if (user_role === 'assistant') {



if (content.length > 0) {
content = md.render(content);

messageEl.innerHTML += `
<div class="assistant-message">
${content}
</div>
`;
try {
const jsonContent = JSON.parse(content);
if (jsonContent.callback && typeof jsonContent.callback === 'object') {
const table = document.createElement('table');
table.className = 'callback-table';
Object.entries(jsonContent.callback).forEach(([key, value]) => {
const row = table.insertRow();
const keyCell = row.insertCell();
const valueCell = row.insertCell();
keyCell.textContent = key;
valueCell.textContent = value;
});

const acceptButton = document.createElement('button');
acceptButton.className = 'callback-accept-button';
acceptButton.textContent = 'Accept suggestion';
acceptButton.addEventListener('click', () => {
if (typeof this.callback === 'function') {
this.callback(jsonContent.callback);
}
});

messageEl.innerHTML += `
<div class="assistant-message">
${table.outerHTML}
${acceptButton.outerHTML}
</div>
`;
} else {
content = md.render(content);
messageEl.innerHTML += `
<div class="assistant-message">
${content}
</div>
`;
}
} catch (e) {
content = md.render(content);
messageEl.innerHTML += `
<div class="assistant-message">
${content}
</div>
`;
}
}
if (Object.values(msg.tool_calls).length > 0) {
const toolCallsContent = msg.tool_calls.map((toolCall, index) => {
Expand Down Expand Up @@ -468,13 +531,4 @@ class MessageRenderer {
}
}
}
}

// Example toggle logic for developer pills
function toggleExpand(messageId)
{
const contentEl = document.getElementById("message-content-" + messageId);
if (contentEl) {
contentEl.classList.toggle('hidden');
}
}
}
12 changes: 9 additions & 3 deletions core/components/aikit/src/API/ConversationsAPI.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ private function handlePostRequest(ServerRequestInterface $request): ResponseInt
{
$body = json_decode((string)$request->getBody(), true);
$body['title'] = $body['title'] ?? 'New conversation';
$body['prompt'] = $body['prompt'] ?? '';

/** @var Conversation $conversation */
$conversation = $this->modx->newObject(Conversation::class);
Expand All @@ -69,7 +70,12 @@ private function handlePostRequest(ServerRequestInterface $request): ResponseInt
return $this->createJsonResponse(['error' => 'Failed to add system prompt'], 500);
}

// @todo allow the creation of a conversation to add its own system prompt (like provide current context)
if (!empty($body['prompt'])) {
$prompt = $body['prompt'] . "\n\n" . 'Return only the result of the prompt as a JSON object. Do not include any text around the JSON structure. The only key in the json object must be "callback". The callback must be a nested json object that contains the requested keys mentioned before.';
if (!$this->addPrompt($conversation, $prompt, Message::ROLE_USER)) {
return $this->createJsonResponse(['error' => 'Failed to add context prompt'], 500);
}
}

return $this->createJsonResponse(['data' => $conversation->toArray()], 201);
}
Expand Down Expand Up @@ -104,7 +110,7 @@ private function createJsonResponse(array $data, int $statusCode): ResponseInter
return $response;
}

private function addPrompt(Conversation $conversation, string $prompt): bool
private function addPrompt(Conversation $conversation, string $prompt, string $role = Message::ROLE_DEVELOPER): bool
{
// Process MODX placeholders and tags
$parser = $this->modx->getParser();
Expand All @@ -115,7 +121,7 @@ private function addPrompt(Conversation $conversation, string $prompt): bool
$message = $this->modx->newObject(Message::class);
$message->fromArray([
'conversation' => $conversation->get('id'),
'user_role' => Message::ROLE_DEVELOPER,
'user_role' => $role,
'user' => 0,
'created_on' => time(),
'content' => $prompt,
Expand Down