From ab0344bc42ece7521b8a31427ed1a777459384cf Mon Sep 17 00:00:00 2001 From: JoseP3r32 <146430742+JoseP3r32@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:33:20 +0200 Subject: [PATCH 01/17] chatCompletions comment line --- server/web/src/utils/api/chatCompletions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/web/src/utils/api/chatCompletions.ts b/server/web/src/utils/api/chatCompletions.ts index 1b5160660..52e06a33b 100644 --- a/server/web/src/utils/api/chatCompletions.ts +++ b/server/web/src/utils/api/chatCompletions.ts @@ -1,6 +1,6 @@ -import { +/*import { defaultApiServer, -} from '@/utils/api'; +} from '@/utils/api';*/ import {OpenAI} from "openai/index"; import {Settings} from "@/state/Settings"; @@ -8,7 +8,7 @@ import {Settings} from "@/state/Settings"; export function openai (settings: Settings): OpenAI { if (!settings.apiKey) throw 'API key not set'; return new OpenAI({ - baseURL: defaultApiServer, + //baseURL: defaultApiServer, // TODO: remove this when the key is the user token used for client auth dangerouslyAllowBrowser: true, apiKey: settings.apiKey, // defaults to process.env["OPENAI_API_KEY"] From 7ef23cfe8f988ada70affba6169ac777d9b6f32d Mon Sep 17 00:00:00 2001 From: JoseP3r32 <146430742+JoseP3r32@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:37:31 +0200 Subject: [PATCH 02/17] Assistants-page-view --- .../Pages/Assistants/Assistants.tsx | 122 ++++++++++++++++++ .../src/components/Pages/Assistants/index.ts | 1 + server/web/src/components/Sidebar/Sidebar.tsx | 6 + server/web/src/main.tsx | 9 ++ server/web/src/utils/api/assistants.ts | 110 ++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 server/web/src/components/Pages/Assistants/Assistants.tsx create mode 100644 server/web/src/components/Pages/Assistants/index.ts create mode 100644 server/web/src/utils/api/assistants.ts diff --git a/server/web/src/components/Pages/Assistants/Assistants.tsx b/server/web/src/components/Pages/Assistants/Assistants.tsx new file mode 100644 index 000000000..a2c8f897f --- /dev/null +++ b/server/web/src/components/Pages/Assistants/Assistants.tsx @@ -0,0 +1,122 @@ +import { useContext, useEffect, useState, ChangeEvent } from "react"; +import { useAuth } from "@/state/Auth"; +import { LoadingContext } from "@/state/Loading"; +import { + getAssistants, + postAssistant, + putAssistant, + deleteAssistant, +} from "@/utils/api/assistants"; // Asegúrate de que tienes estas funciones en tus utils de API +import { + Alert, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Grid, + Paper, + Snackbar, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography +} from "@mui/material"; + +// Asumiendo que tienes un tipo definido para Assistant similar a tu tipo OrganizationResponse +type Assistant = { + id: number; + name: string; + createdAt: string; +}; + +const emptyAssistant: Assistant = { + id: 0, + name: "", + createdAt: "" +}; + +export function Assistants() { + const auth = useAuth(); + const [loading, setLoading] = useContext(LoadingContext); + const [assistants, setAssistants] = useState([]); + const [showAlert, setShowAlert] = useState(''); + const [selectedAssistant, setSelectedAssistant] = useState(emptyAssistant); + const [openEditDialog, setOpenEditDialog] = useState(false); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + + async function loadAssistants() { + setLoading(true); + try { + const response = await getAssistants(auth.authToken); + setAssistants(response); + } catch (error) { + console.error('Error fetching assistants:', error); + setShowAlert('Failed to load assistants.'); + } + setLoading(false); + } + + useEffect(() => { + loadAssistants(); + }, []); + + // Agrega aquí las funciones para manejar la creación, edición y eliminación, siguiendo el ejemplo de tu componente de Organizations + + return ( + + + Assistants + + + {loading ? ( + Loading... + ) : ( + assistants.length === 0 ? ( + No assistants available. + ) : ( + + + + + Date + Name + Actions + + + + {assistants.map((assistant) => ( + + {assistant.createdAt} + {assistant.name} + + + + + + ))} + +
+
+ ) + )} + {/* Add/Edit Assistant Dialog */} + {/* Delete Assistant Dialog */} + {/* Alert Snackbar */} +
+ ); +} diff --git a/server/web/src/components/Pages/Assistants/index.ts b/server/web/src/components/Pages/Assistants/index.ts new file mode 100644 index 000000000..02d1533d3 --- /dev/null +++ b/server/web/src/components/Pages/Assistants/index.ts @@ -0,0 +1 @@ +export * from './Assistants'; diff --git a/server/web/src/components/Sidebar/Sidebar.tsx b/server/web/src/components/Sidebar/Sidebar.tsx index 0d8e15b51..74bc9f268 100644 --- a/server/web/src/components/Sidebar/Sidebar.tsx +++ b/server/web/src/components/Sidebar/Sidebar.tsx @@ -43,6 +43,12 @@ export function Sidebar({ drawerWidth, open }: SidebarProps) { + + + {/* Puedes agregar un ícono si es necesario, por ejemplo */} + + + diff --git a/server/web/src/main.tsx b/server/web/src/main.tsx index 3427b940e..a2c36d3bc 100644 --- a/server/web/src/main.tsx +++ b/server/web/src/main.tsx @@ -8,6 +8,7 @@ import { App } from '@/components/App'; import { Root } from '@/components/Pages/Root'; import { ErrorPage } from '@/components/Pages/ErrorPage'; import { Organizations } from '@/components/Pages/Organizations'; +import { Assistants } from '@/components/Pages/Assistants'; import { Chat } from '@/components/Pages/Chat'; import { GenericQuestion } from '@/components/Pages/GenericQuestion'; import { SettingsPage } from '@/components/Pages/SettingsPage'; @@ -54,6 +55,14 @@ const router = createBrowserRouter([ ), }, + { + path: 'assistants', + element: ( + + + + ), + }, { path: 'projects', element: ( diff --git a/server/web/src/utils/api/assistants.ts b/server/web/src/utils/api/assistants.ts new file mode 100644 index 000000000..f7f5556fd --- /dev/null +++ b/server/web/src/utils/api/assistants.ts @@ -0,0 +1,110 @@ +// Este código asume que tienes un enum similar para los endpoints de la API de asistentes +import { + ApiOptions, + EndpointsEnum, + apiConfigConstructor, + apiFetch, + baseHeaders, + defaultApiServer, +} from '@/utils/api'; + +// Definiciones de tipos para las respuestas y solicitudes de asistentes (ajústalas a tu API) +export type AssistantRequest = { + name: string; + // Otros campos relevantes para la creación o actualización de un asistente +}; + +export type AssistantResponse = { + id: number; + name: string; + createdAt: string; + // Otros campos que tu API devuelve +}; + +const assistantApiBaseOptions: ApiOptions = { + endpointServer: defaultApiServer, + endpointPath: EndpointsEnum.assistant, // Asegúrate de que este enum existe y es correcto + endpointValue: '', + requestOptions: { + headers: baseHeaders, + }, +}; + +// POST: Crear un nuevo asistente +export async function postAssistant(authToken: string, data: AssistantRequest): Promise { + const apiOptions: ApiOptions = { + ...assistantApiBaseOptions, + body: JSON.stringify(data), + requestOptions: { + method: 'POST', + ...assistantApiBaseOptions.requestOptions, + headers: { + ...assistantApiBaseOptions.requestOptions.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const apiConfig = apiConfigConstructor(apiOptions); + const response = await apiFetch(apiConfig); + return response.status; // Asumiendo que la API devuelve un código de estado para representar el resultado +} + +// GET: Obtener todos los asistentes +export async function getAssistants(authToken: string): Promise { + const apiOptions: ApiOptions = { + ...assistantApiBaseOptions, + requestOptions: { + method: 'GET', + ...assistantApiBaseOptions.requestOptions, + headers: { + ...assistantApiBaseOptions.requestOptions.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const apiConfig = apiConfigConstructor(apiOptions); + const response = await apiFetch(apiConfig); + if (!response.data) { + throw new Error('No assistants data returned from the API'); + } + return response.data; +} + +// PUT: Actualizar un asistente existente +export async function putAssistant(authToken: string, id: number, data: AssistantRequest): Promise { + const apiOptions: ApiOptions = { + ...assistantApiBaseOptions, + endpointValue: `/${id}`, + body: JSON.stringify(data), + requestOptions: { + method: 'PUT', + ...assistantApiBaseOptions.requestOptions, + headers: { + ...assistantApiBaseOptions.requestOptions.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const apiConfig = apiConfigConstructor(apiOptions); + const response = await apiFetch(apiConfig); + return response.status; +} + +// DELETE: Eliminar un asistente +export async function deleteAssistant(authToken: string, id: number): Promise { + const apiOptions: ApiOptions = { + ...assistantApiBaseOptions, + endpointValue: `/${id}`, + requestOptions: { + method: 'DELETE', + ...assistantApiBaseOptions.requestOptions, + headers: { + ...assistantApiBaseOptions.requestOptions.headers, + Authorization: `Bearer ${authToken}`, + }, + }, + }; + const apiConfig = apiConfigConstructor(apiOptions); + const response = await apiFetch(apiConfig); + return response.status; +} From b9fe17cd881d4399cdbea16ed0e25f1ac1a1e7e3 Mon Sep 17 00:00:00 2001 From: JoseP3r32 <146430742+JoseP3r32@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:29:31 +0200 Subject: [PATCH 03/17] assistant create form update --- .../Pages/Assistants/Assistants.tsx | 305 ++++++++++++++---- server/web/src/xef Dashboard.html | 20 ++ 2 files changed, 254 insertions(+), 71 deletions(-) create mode 100644 server/web/src/xef Dashboard.html diff --git a/server/web/src/components/Pages/Assistants/Assistants.tsx b/server/web/src/components/Pages/Assistants/Assistants.tsx index a2c8f897f..d6e58d6da 100644 --- a/server/web/src/components/Pages/Assistants/Assistants.tsx +++ b/server/web/src/components/Pages/Assistants/Assistants.tsx @@ -1,35 +1,42 @@ import { useContext, useEffect, useState, ChangeEvent } from "react"; import { useAuth } from "@/state/Auth"; import { LoadingContext } from "@/state/Loading"; + import { getAssistants, postAssistant, putAssistant, deleteAssistant, -} from "@/utils/api/assistants"; // Asegúrate de que tienes estas funciones en tus utils de API +} from "@/utils/api/assistants"; import { Alert, - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - Grid, - Paper, - Snackbar, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Typography + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControlLabel, + FormGroup, + MenuItem, + Grid, + Divider, + Paper, + Switch, + Snackbar, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Slider } from "@mui/material"; -// Asumiendo que tienes un tipo definido para Assistant similar a tu tipo OrganizationResponse type Assistant = { id: number; name: string; @@ -50,6 +57,12 @@ export function Assistants() { const [selectedAssistant, setSelectedAssistant] = useState(emptyAssistant); const [openEditDialog, setOpenEditDialog] = useState(false); const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [showCreatePanel, setShowCreatePanel] = useState(false); + const [fileSearchEnabled, setFileSearchEnabled] = useState(false); + const [codeInterpreterEnabled, setCodeInterpreterEnabled] = useState(false); + const [JsonObjectEnabled, setJsonObjectEnabled] = useState(false); + const [temperature, setTemperature] = useState(1); + const [topP, setTopP] = useState(1); async function loadAssistants() { setLoading(true); @@ -67,56 +80,206 @@ export function Assistants() { loadAssistants(); }, []); - // Agrega aquí las funciones para manejar la creación, edición y eliminación, siguiendo el ejemplo de tu componente de Organizations + const models = [ + { + value: 'gpt-4-turbo', + label: 'gpt-4-turbo', + }, + { + value: 'gpt-4', + label: 'gpt-4', + }, + { + value: 'gpt-3.5-turbo-16k', + label: 'gpt-3.5-turbo-16k', + }, + { + value: 'gpt-3.5-turbo-0125', + label: 'gpt-3.5-turbo-0125', + }, + { + value: 'gpt-3.5-turbo', + label: 'gpt-3.5-turbo', + }, + { + value: 'gpt-3.5-turbo', + label: 'gpt-3.5-turbo', + }, + ]; + + const handleCreateAssistant = async () => { + // Aquí se incluirá la lógica para crear un nuevo asistente + // Por ejemplo: await postAssistant(authToken, { name: selectedAssistant.name }); + }; + + const handleFileSearchChange = (event: ChangeEvent) => { + setFileSearchEnabled(event.target.checked); + }; + + const handleCodeInterpreterChange = (event: ChangeEvent) => { + setCodeInterpreterEnabled(event.target.checked); + }; + + const handleJsonObjectChange = (event: ChangeEvent) => { + setJsonObjectEnabled(event.target.checked); + }; + + const handleTemperatureChange = (event: Event, newValue: number | number[]) => { + setTemperature(newValue as number); + }; + const handleTopPChange = (event: Event, newValue: number | number[]) => { + setTopP(newValue as number); + }; + const handleFilesButtonClick = (toolName: String)=> { + console.log(`Clicked on ${toolName} button`); + }; return ( - - - Assistants - - - {loading ? ( - Loading... - ) : ( - assistants.length === 0 ? ( - No assistants available. - ) : ( - - - - - Date - Name - Actions - - - - {assistants.map((assistant) => ( - - {assistant.createdAt} - {assistant.name} - - - - - - ))} - -
-
- ) - )} - {/* Add/Edit Assistant Dialog */} - {/* Delete Assistant Dialog */} - {/* Alert Snackbar */} -
- ); -} + + + {showCreatePanel && ( + + + + {selectedAssistant.id ? 'Edit Assistant' : 'Create Assistant'} + + + setSelectedAssistant({ ...selectedAssistant, name: e.target.value })} + margin="normal" + /> + + setSelectedAssistant({ ...selectedAssistant, model: e.target.value })} + margin="normal" + sx={{ display: 'block', mt: 3, mb: 3 }} + SelectProps={{ + native: true, + }} + helperText="Please select your model" + > + {models.map((option) => ( + + ))} + + + Tools + + } + label="File search" + sx={{ display: 'block', mt: 2, mb: 2 }} + /> + + } + label="Code interpreter" + sx={{ display: 'block', mt: 2, mb: 2 }} + /> + + Functions + + MODEL CONFIGURATION + + Response format + + } + label="JSON object" + sx={{ display: 'block', mt: 1, mb: 1 }} + /> + Temperature + + Top P + + + + + )} + {/* Vista principal */} + + + Assistants + + + {loading ? ( + Loading... + ) : ( + assistants.length === 0 ? ( + No assistants available. + ) : ( + + + + + Date + Name + Actions + + + + {assistants.map((assistant) => ( + + {assistant.createdAt} + {assistant.name} + + + + + + ))} + +
+
+ ) + )} + {/* Diálogos de edición y eliminación */} + {/* Snackbar de alerta */} +
+
+ ); + } diff --git a/server/web/src/xef Dashboard.html b/server/web/src/xef Dashboard.html new file mode 100644 index 000000000..e3f499e32 --- /dev/null +++ b/server/web/src/xef Dashboard.html @@ -0,0 +1,20 @@ + + + + + + + + + + + xef Dashboard + + +
+ + + From 2227379e4f6883d9acf0c2ec8fb2ce09ad2d737a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20P=C3=A9rez=20Pacheco?= Date: Tue, 30 Apr 2024 20:05:25 +0200 Subject: [PATCH 04/17] Tokens in Evaluator Tests (#730) --- .../com/xebia/functional/xef/llm/Chat.kt | 57 +++++++++--- .../functional/xef/llm/models/Messages.kt | 18 ++++ .../functional/xef/evaluator/SuiteBuilder.kt | 1 + .../functional/xef/evaluator/models/Html.kt | 15 ++-- .../xef/evaluator/models/ItemResult.kt | 1 + .../xef/evaluator/models/Markdown.kt | 10 +++ .../xef/evaluator/models/TestModels.kt | 28 +++++- evaluator/src/main/resources/web/index.html | 13 --- evaluator/src/main/resources/web/script.js | 65 -------------- evaluator/src/main/resources/web/style.css | 87 ------------------- .../functional/xef/evaluator/TestExample.kt | 21 +++-- 11 files changed, 123 insertions(+), 193 deletions(-) create mode 100644 core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/Messages.kt delete mode 100644 evaluator/src/main/resources/web/index.html delete mode 100644 evaluator/src/main/resources/web/script.js delete mode 100644 evaluator/src/main/resources/web/style.css diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt index 3dc16adb8..60ab52f26 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/Chat.kt @@ -2,11 +2,17 @@ package com.xebia.functional.xef.llm import com.xebia.functional.openai.generated.api.Chat import com.xebia.functional.openai.generated.model.CreateChatCompletionRequest +import com.xebia.functional.openai.generated.model.CreateChatCompletionResponse +import com.xebia.functional.openai.generated.model.CreateChatCompletionResponseChoicesInner import com.xebia.functional.xef.AIError import com.xebia.functional.xef.conversation.AiDsl import com.xebia.functional.xef.conversation.Conversation +import com.xebia.functional.xef.llm.models.MessageWithUsage +import com.xebia.functional.xef.llm.models.MessagesUsage +import com.xebia.functional.xef.llm.models.MessagesWithUsage import com.xebia.functional.xef.prompt.Prompt import com.xebia.functional.xef.prompt.PromptBuilder +import com.xebia.functional.xef.store.Memory import kotlinx.coroutines.flow.* @AiDsl @@ -54,9 +60,34 @@ suspend fun Chat.promptMessage(prompt: Prompt, scope: Conversation = Conversatio suspend fun Chat.promptMessages( prompt: Prompt, scope: Conversation = Conversation() -): List = +): List = promptResponse(prompt, scope) { it.message.content }.first + +@AiDsl +suspend fun Chat.promptMessageAndUsage( + prompt: Prompt, + scope: Conversation = Conversation() +): MessageWithUsage { + val response = promptMessagesAndUsage(prompt, scope) + val message = response.messages.firstOrNull() ?: throw AIError.NoResponse() + return MessageWithUsage(message, response.usage) +} + +@AiDsl +suspend fun Chat.promptMessagesAndUsage( + prompt: Prompt, + scope: Conversation = Conversation() +): MessagesWithUsage { + val response = promptResponse(prompt, scope) { it.message.content } + return MessagesWithUsage(response.first, response.second.usage?.let { MessagesUsage(it) }) +} + +private suspend fun Chat.promptResponse( + prompt: Prompt, + scope: Conversation = Conversation(), + block: suspend Chat.(CreateChatCompletionResponseChoicesInner) -> T? +): Pair, CreateChatCompletionResponse> = scope.metric.promptSpan(prompt) { - val promptMemories = prompt.messages.toMemory(scope) + val promptMemories: List = prompt.messages.toMemory(scope) val adaptedPrompt = PromptCalculator.adaptPromptToConversationAndModel(prompt, scope) adaptedPrompt.addMetrics(scope) @@ -72,13 +103,17 @@ suspend fun Chat.promptMessages( seed = adaptedPrompt.configuration.seed, ) - createChatCompletion(request) - .addMetrics(scope) - .choices - .addChoiceToMemory( - scope, - promptMemories, - prompt.configuration.messagePolicy.addMessagesToConversation - ) - .mapNotNull { it.message.content } + val createResponse: CreateChatCompletionResponse = createChatCompletion(request) + Pair( + createResponse + .addMetrics(scope) + .choices + .addChoiceToMemory( + scope, + promptMemories, + prompt.configuration.messagePolicy.addMessagesToConversation + ) + .mapNotNull { block(it) }, + createResponse + ) } diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/Messages.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/Messages.kt new file mode 100644 index 000000000..1817d3f16 --- /dev/null +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/Messages.kt @@ -0,0 +1,18 @@ +package com.xebia.functional.xef.llm.models + +import com.xebia.functional.openai.generated.model.CompletionUsage + +data class MessagesWithUsage(val messages: List, val usage: MessagesUsage?) + +data class MessageWithUsage(val message: String, val usage: MessagesUsage?) + +data class MessagesUsage(val completionTokens: Int, val promptTokens: Int, val totalTokens: Int) { + companion object { + operator fun invoke(usage: CompletionUsage) = + MessagesUsage( + completionTokens = usage.completionTokens, + promptTokens = usage.promptTokens, + totalTokens = usage.totalTokens + ) + } +} diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt index 1df855b86..94a33497b 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt @@ -44,6 +44,7 @@ data class SuiteSpec( output.description.value, item.context, output.value, + output.tokens, classification, success.contains(classification) ) diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt index d4eecb719..a2b1c377c 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt @@ -56,10 +56,12 @@ value class Html(val value: String) { const outputDiv = document.createElement('pre'); outputDiv.classList.add('output'); outputDiv.innerText = 'Output: ' + test.output; - outputDiv.addEventListener('click', function() { - this.classList.toggle('expanded'); - }); blockDiv.appendChild(outputDiv); + + const usageDiv = document.createElement('pre'); + usageDiv.classList.add('output'); + usageDiv.innerText = 'Usage: \n Completion Tokens: ' + test.usage?.completionTokens + '\n Prompt Tokens: ' + test.usage?.promptTokens + '\n Total Tokens: ' + test.usage?.totalTokens; + blockDiv.appendChild(usageDiv); const result = document.createElement('div'); result.classList.add('score', test.success ? 'score-passed' : 'score-failed'); @@ -123,16 +125,11 @@ value class Html(val value: String) { .output { color: #666; - cursor: pointer; - white-space: nowrap; + white-space: normal; overflow: hidden; text-overflow: ellipsis; } - .output.expanded { - white-space: normal; - } - .score { font-weight: bold; } diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ItemResult.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ItemResult.kt index f78d6646a..19e2295e2 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ItemResult.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ItemResult.kt @@ -21,6 +21,7 @@ data class OutputResult( val description: String, val contextDescription: String, val output: String, + val usage: OutputTokens?, val result: E, val success: Boolean ) where E : AI.PromptClassifier, E : Enum diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt index 40a275b11..a998f08e2 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt @@ -28,6 +28,16 @@ value class Markdown(val value: String) { |
|${outputResult.output} |
+ |- Usage: + |
+ |${outputResult.usage?.let { usage -> + """ + |Completion Tokens: ${usage.completionTokens} + |Prompt Tokens: ${usage.promptTokens} + |Total Tokens: ${usage.totalTokens} + """.trimMargin() + } ?: "No usage information available"} + |
| |Result: ${if (outputResult.success) "✅ Success" else "❌ Failure"} (${outputResult.result}) """.trimMargin() diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt index 77f6157bb..ca9160674 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt @@ -1,17 +1,39 @@ package com.xebia.functional.xef.evaluator.models +import com.xebia.functional.xef.llm.models.MessageWithUsage +import com.xebia.functional.xef.llm.models.MessagesUsage import kotlin.jvm.JvmSynthetic import kotlinx.serialization.Serializable @Serializable data class OutputDescription(val value: String) @Serializable -data class OutputResponse(val description: OutputDescription, val value: String) { +data class OutputResponse( + val description: OutputDescription, + val tokens: OutputTokens?, + val value: String +) { companion object { @JvmSynthetic suspend operator fun invoke( description: OutputDescription, - block: suspend () -> String - ): OutputResponse = OutputResponse(description, block()) + block: suspend () -> MessageWithUsage + ): OutputResponse { + val response = block() + return OutputResponse(description, response.usage?.let { OutputTokens(it) }, response.message) + } + } +} + +@Serializable +data class OutputTokens( + val promptTokens: Int? = null, + val completionTokens: Int? = null, + val totalTokens: Int? = null +) { + companion object { + @JvmSynthetic + operator fun invoke(usage: MessagesUsage): OutputTokens = + OutputTokens(usage.promptTokens, usage.completionTokens, usage.totalTokens) } } diff --git a/evaluator/src/main/resources/web/index.html b/evaluator/src/main/resources/web/index.html deleted file mode 100644 index 2a3718117..000000000 --- a/evaluator/src/main/resources/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Tests - - - - - -
- - diff --git a/evaluator/src/main/resources/web/script.js b/evaluator/src/main/resources/web/script.js deleted file mode 100644 index 35dc55966..000000000 --- a/evaluator/src/main/resources/web/script.js +++ /dev/null @@ -1,65 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - - const container = document.getElementById('test-container'); - - const headerDiv = document.createElement('div'); - headerDiv.classList.add('test-block'); - - const header = document.createElement('h1'); - header.classList.add('test-header'); - header.textContent = "Suite test"; - - const suiteDescription = document.createElement('p'); - suiteDescription.textContent = 'Description: ' + testData.description; - - const model = document.createElement('p'); - model.textContent = 'Model: ' + testData.model; - - const metric = document.createElement('p'); - metric.textContent = 'Metric: ' + testData.metric; - - headerDiv.appendChild(header); - headerDiv.appendChild(suiteDescription); - headerDiv.appendChild(model); - headerDiv.appendChild(metric); - - container.appendChild(headerDiv); - - testData.items.forEach(block => { - const blockDiv = document.createElement('div'); - blockDiv.classList.add('test-block'); - - const title = document.createElement('h2'); - title.classList.add('test-title'); - title.textContent = 'Input: ' + block.description; - - blockDiv.appendChild(title); - - block.items.forEach(test => { - const itemDescription = document.createElement('div'); - itemDescription.textContent = 'Description: ' + test.description; - blockDiv.appendChild(itemDescription); - - const context = document.createElement('div'); - context.textContent = 'Context: ' + test.contextDescription; - blockDiv.appendChild(context); - - const outputDiv = document.createElement('pre'); - outputDiv.classList.add('output'); - outputDiv.innerText = 'Output: ' + test.output; - outputDiv.addEventListener('click', function() { - this.classList.toggle('expanded'); - }); - blockDiv.appendChild(outputDiv); - - const result = document.createElement('div'); - result.classList.add('score', test.success ? 'score-passed' : 'score-failed'); - result.textContent = 'Result: ' + test.result; - blockDiv.appendChild(result); - - blockDiv.appendChild(document.createElement('br')); - }); - container.appendChild(blockDiv); - }); - -}); diff --git a/evaluator/src/main/resources/web/style.css b/evaluator/src/main/resources/web/style.css deleted file mode 100644 index a14683826..000000000 --- a/evaluator/src/main/resources/web/style.css +++ /dev/null @@ -1,87 +0,0 @@ -body { - font-family: Arial, sans-serif; - margin: 0; - padding: 0; - background-color: #f4f4f4; -} - -#test-container { - width: 80%; - margin: 20px auto; - padding: 15px; - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.test-block { - margin-bottom: 20px; - border-bottom: 1px solid #eee; - padding-bottom: 20px; -} - -.test-title { - font-size: 1.2em; - color: #333; -} - -.input, .output { - margin: 5px 0; -} - -.input-passed { - margin-top: 25px; - color: green; - font-weight: bold; -} - -.input-failed { - margin-top: 25px; - color: red; - font-weight: bold; -} - -.output { - color: #666; - cursor: pointer; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.output.expanded { - white-space: normal; -} - -.score { - font-weight: bold; -} - -.score-passed { - margin-bottom: 25px; - color: #008000; -} - -.score-failed { - margin-bottom: 25px; - color: red; -} - -.avg-score, .test-info { - font-size: 1.2em; - color: #d35400; - margin-top: 10px; -} - -.test-summary { - background-color: #e7e7e7; - padding: 15px; - margin-top: 20px; - border-radius: 8px; -} - -.test-summary h3 { - font-size: 1.1em; - color: #555; - margin-top: 0; -} diff --git a/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt b/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt index fb8ce532b..d51a6710d 100644 --- a/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt +++ b/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt @@ -7,9 +7,10 @@ import com.xebia.functional.xef.conversation.Conversation import com.xebia.functional.xef.evaluator.metrics.AnswerAccuracy import com.xebia.functional.xef.evaluator.models.OutputDescription import com.xebia.functional.xef.evaluator.models.OutputResponse -import com.xebia.functional.xef.llm.promptMessage +import com.xebia.functional.xef.llm.promptMessageAndUsage import com.xebia.functional.xef.prompt.Prompt import com.xebia.functional.xef.prompt.PromptBuilder.Companion.user +import java.io.File object TestExample { @@ -31,10 +32,10 @@ object TestExample { context = "Contains information about a movie" ) { +OutputResponse(gpt35Description) { - Conversation { chat.promptMessage(Prompt(model) { +user(input) }) } + Conversation { chat.promptMessageAndUsage(Prompt(model) { +user(input) }) } } - +OutputResponse(description = fakeOutputs, value = "I don't know") + +OutputResponse(description = fakeOutputs, null, value = "I don't know") } +ItemSpec( @@ -42,13 +43,22 @@ object TestExample { context = "Contains instructions for making a cake" ) { +OutputResponse(gpt35Description) { - Conversation { chat.promptMessage(Prompt(model) { +user(input) }) } + Conversation { chat.promptMessageAndUsage(Prompt(model) { +user(input) }) } } - +OutputResponse(description = fakeOutputs, value = "The movie is Jurassic Park") + +OutputResponse(description = fakeOutputs, null, value = "The movie is Jurassic Park") } } val results = spec.evaluate(success = listOf(AnswerAccuracy.yes)) + + val outputPath = System.getProperty("user.dir") + "/build/testSuite" + File(outputPath).mkdir() + val fileHtml = File("$outputPath/test.html") + fileHtml.writeText(SuiteSpec.toHtml(results, "test.html").value) + + val fileMarkDown = File("$outputPath/test.md") + fileMarkDown.writeText(SuiteSpec.toMarkdown(results, "test.md").value) + results.items.forEach { println("==============") println(" ${it.description}") @@ -57,6 +67,7 @@ object TestExample { println() println(">> Output ${index + 1}") println("Description: ${item.description}") + println("Usage: ${item.usage}") println("Success: ${item.success}") println() println("AI Output:") From e1b68076a1ef7f3ee9505c9ca35601f41eb6bfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20P=C3=A9rez=20Pacheco?= Date: Thu, 2 May 2024 18:44:06 +0200 Subject: [PATCH 05/17] Estimate price in Evaluator Tests (#731) --- .../functional/xef/evaluator/SuiteBuilder.kt | 7 ++- .../functional/xef/evaluator/models/Html.kt | 14 ++++-- .../xef/evaluator/models/Markdown.kt | 7 ++- .../xef/evaluator/models/ModelsPricing.kt | 44 +++++++++++++++++++ .../xef/evaluator/models/TestModels.kt | 33 ++++++++++++-- .../functional/xef/evaluator/TestExample.kt | 5 ++- 6 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ModelsPricing.kt diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt index 94a33497b..ff85f9e95 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/SuiteBuilder.kt @@ -68,10 +68,9 @@ data class SuiteSpec( E : Enum = Html.get(Json.encodeToString(SuiteResults.serializer(serializer()), result), suiteName) - inline fun toMarkdown( - result: SuiteResults, - suiteName: String, - ): Markdown where E : AI.PromptClassifier, E : Enum = Markdown.get(result, suiteName) + inline fun toMarkdown(result: SuiteResults, suiteName: String): Markdown where + E : AI.PromptClassifier, + E : Enum = Markdown.get(result, suiteName) } } diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt index a2b1c377c..b2514dfbb 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Html.kt @@ -58,10 +58,12 @@ value class Html(val value: String) { outputDiv.innerText = 'Output: ' + test.output; blockDiv.appendChild(outputDiv); - const usageDiv = document.createElement('pre'); - usageDiv.classList.add('output'); - usageDiv.innerText = 'Usage: \n Completion Tokens: ' + test.usage?.completionTokens + '\n Prompt Tokens: ' + test.usage?.promptTokens + '\n Total Tokens: ' + test.usage?.totalTokens; - blockDiv.appendChild(usageDiv); + if (test.usage != undefined) { + const usageDiv = document.createElement('pre'); + usageDiv.classList.add('output'); + usageDiv.innerText = 'Usage: \n Prompt Tokens: ' + test.usage?.promptTokens + ' (~' + test.usage?.estimatePricePerToken + ' ' + test.usage?.currency + ')\n Completion Tokens: ' + test.usage?.completionTokens + ' (~' + test.usage?.estimatePriceCompletionToken + ' ' + test.usage?.currency + ')\n Total Tokens: ' + test.usage?.totalTokens + '\n Total Price: ~' + test.usage?.estimatePriceTotalToken + ' ' + test.usage?.currency; + blockDiv.appendChild(usageDiv); + } const result = document.createElement('div'); result.classList.add('score', test.success ? 'score-passed' : 'score-failed'); @@ -101,6 +103,10 @@ value class Html(val value: String) { border-bottom: 1px solid #eee; padding-bottom: 20px; } + + .test-block pre { + margin-bottom: 20px; + } .test-title { font-size: 1.2em; diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt index a998f08e2..92baaaf7b 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/Markdown.kt @@ -32,9 +32,10 @@ value class Markdown(val value: String) { |
|${outputResult.usage?.let { usage -> """ - |Completion Tokens: ${usage.completionTokens} - |Prompt Tokens: ${usage.promptTokens} + |Prompt Tokens: ${usage.promptTokens} ${usage.estimatePricePerToken?.let { "(~ ${it.to2DecimalsString()} ${usage.currency ?: ""})" } ?: "" } + |Completion Tokens: ${usage.completionTokens} ${usage.estimatePriceCompletionToken?.let { "(~ ${it.to2DecimalsString()} ${usage.currency ?: ""})" } ?: "" } |Total Tokens: ${usage.totalTokens} + |Total Price: ${usage.estimatePriceTotalToken?.let { "${it.to2DecimalsString()} ${usage.currency ?: ""}" } ?: "Unknown"} """.trimMargin() } ?: "No usage information available"} |
@@ -50,5 +51,7 @@ value class Markdown(val value: String) { .trimMargin() return Markdown(content) } + + private fun Double.to2DecimalsString() = String.format("%.6f", this) } } diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ModelsPricing.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ModelsPricing.kt new file mode 100644 index 000000000..124148c84 --- /dev/null +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/ModelsPricing.kt @@ -0,0 +1,44 @@ +package com.xebia.functional.xef.evaluator.models + +data class ModelsPricing( + val modelName: String, + val currency: String, + val input: ModelsPricingItem, + val output: ModelsPricingItem +) { + + companion object { + + const val oneMillion = 1_000_000 + val oneThousand = 1_000 + + // The pricing for the models was updated the May 2st, 2024 + // Be sure to update the pricing for each model + + val gpt4Turbo = + ModelsPricing( + modelName = "gpt-4-turbo", + currency = "USD", + input = ModelsPricingItem(10.0, oneMillion), + output = ModelsPricingItem(30.0, oneMillion) + ) + + val gpt4 = + ModelsPricing( + modelName = "gpt-4-turbo", + currency = "USD", + input = ModelsPricingItem(30.0, oneMillion), + output = ModelsPricingItem(60.0, oneMillion) + ) + + val gpt3_5Turbo = + ModelsPricing( + modelName = "gpt-3.5-turbo", + currency = "USD", + input = ModelsPricingItem(0.5, oneMillion), + output = ModelsPricingItem(1.5, oneMillion) + ) + } +} + +data class ModelsPricingItem(val price: Double, val perTokens: Int) diff --git a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt index ca9160674..ccdcff017 100644 --- a/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt +++ b/evaluator/src/main/kotlin/com/xebia/functional/xef/evaluator/models/TestModels.kt @@ -17,10 +17,15 @@ data class OutputResponse( @JvmSynthetic suspend operator fun invoke( description: OutputDescription, + price: ModelsPricing?, block: suspend () -> MessageWithUsage ): OutputResponse { val response = block() - return OutputResponse(description, response.usage?.let { OutputTokens(it) }, response.message) + return OutputResponse( + description, + response.usage?.let { OutputTokens(it, price) }, + response.message + ) } } } @@ -28,12 +33,32 @@ data class OutputResponse( @Serializable data class OutputTokens( val promptTokens: Int? = null, + val estimatePricePerToken: Double? = null, val completionTokens: Int? = null, - val totalTokens: Int? = null + val estimatePriceCompletionToken: Double? = null, + val totalTokens: Int? = null, + val estimatePriceTotalToken: Double? = null, + val currency: String? ) { companion object { @JvmSynthetic - operator fun invoke(usage: MessagesUsage): OutputTokens = - OutputTokens(usage.promptTokens, usage.completionTokens, usage.totalTokens) + operator fun invoke(usage: MessagesUsage, price: ModelsPricing?): OutputTokens { + val estimateInputPrice = + price?.let { usage.promptTokens.let { (it * price.input.price) / price.input.perTokens } } + val estimateOutputPrice = + price?.let { + usage.completionTokens.let { (it * price.output.price) / price.output.perTokens } + } + val estimateTotalPrice = estimateInputPrice?.plus(estimateOutputPrice ?: 0.0) + return OutputTokens( + usage.promptTokens, + estimateInputPrice, + usage.completionTokens, + estimateOutputPrice, + usage.totalTokens, + estimateTotalPrice, + price?.currency + ) + } } } diff --git a/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt b/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt index d51a6710d..999a6b8da 100644 --- a/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt +++ b/examples/src/main/kotlin/com/xebia/functional/xef/evaluator/TestExample.kt @@ -5,6 +5,7 @@ import com.xebia.functional.openai.generated.model.CreateChatCompletionRequestMo import com.xebia.functional.xef.OpenAI import com.xebia.functional.xef.conversation.Conversation import com.xebia.functional.xef.evaluator.metrics.AnswerAccuracy +import com.xebia.functional.xef.evaluator.models.ModelsPricing import com.xebia.functional.xef.evaluator.models.OutputDescription import com.xebia.functional.xef.evaluator.models.OutputResponse import com.xebia.functional.xef.llm.promptMessageAndUsage @@ -31,7 +32,7 @@ object TestExample { input = "Please provide a movie title, genre and director", context = "Contains information about a movie" ) { - +OutputResponse(gpt35Description) { + +OutputResponse(gpt35Description, ModelsPricing.gpt3_5Turbo) { Conversation { chat.promptMessageAndUsage(Prompt(model) { +user(input) }) } } @@ -42,7 +43,7 @@ object TestExample { input = "Recipe for a chocolate cake", context = "Contains instructions for making a cake" ) { - +OutputResponse(gpt35Description) { + +OutputResponse(gpt35Description, ModelsPricing.gpt3_5Turbo) { Conversation { chat.promptMessageAndUsage(Prompt(model) { +user(input) }) } } From 6a3fa589edd1ea0561696217625e92a38b1e39c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Monta=C3=B1ez?= Date: Thu, 2 May 2024 22:23:36 +0200 Subject: [PATCH 06/17] Enum description in tools (#732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * support enum descriptions * added example --------- Co-authored-by: José Carlos Montañez Co-authored-by: Raúl Raja Martínez --- .../xef/llm/models/functions/JsonSchema.kt | 35 +++++++++--------- .../xef/assistants/ToolsWithDescriptions.kt | 36 +++++++++++++++++++ 2 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 examples/src/main/kotlin/com/xebia/functional/xef/assistants/ToolsWithDescriptions.kt diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt index 573caa7a4..e7211c2aa 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt @@ -307,6 +307,7 @@ private fun SerialDescriptor.createJsonSchema( } } +@OptIn(ExperimentalSerializationApi::class) private fun JsonObjectBuilder.applyJsonSchemaDefaults( descriptor: SerialDescriptor, annotations: List, @@ -322,25 +323,25 @@ private fun JsonObjectBuilder.applyJsonSchemaDefaults( } } - if (descriptor.kind == SerialKind.ENUM) { - this["enum"] = descriptor.elementNames - } - - if (annotations.isNotEmpty()) { - val multiplatformDescription = annotations.filterIsInstance() - val description = - if (multiplatformDescription.isEmpty()) { - try { - val jvmDescription = annotations.filterIsInstance() - jvmDescription.firstOrNull()?.value - } catch (e: Throwable) { - null + val additionalEnumDescription: String? = + if (descriptor.kind == SerialKind.ENUM) { + this["enum"] = descriptor.elementNames + descriptor.elementNames + .mapIndexed { index, name -> + "$name (${descriptor.getElementAnnotations(index).lastOfInstance()?.value})" } - } else { - multiplatformDescription.firstOrNull()?.value - } + .joinToString("\n - ") + } else null - this["description"] = description + if (annotations.isNotEmpty()) { + val description = annotations.filterIsInstance().firstOrNull()?.value + if (additionalEnumDescription != null) { + this["description"] = "$description\n - $additionalEnumDescription" + } else { + this["description"] = description + } + } else if (additionalEnumDescription != null) { + this["description"] = " - $additionalEnumDescription" } } diff --git a/examples/src/main/kotlin/com/xebia/functional/xef/assistants/ToolsWithDescriptions.kt b/examples/src/main/kotlin/com/xebia/functional/xef/assistants/ToolsWithDescriptions.kt new file mode 100644 index 000000000..cb9a573e0 --- /dev/null +++ b/examples/src/main/kotlin/com/xebia/functional/xef/assistants/ToolsWithDescriptions.kt @@ -0,0 +1,36 @@ +package com.xebia.functional.xef.assistants + +import com.xebia.functional.xef.conversation.Description +import com.xebia.functional.xef.llm.assistants.Assistant +import com.xebia.functional.xef.llm.assistants.AssistantThread +import com.xebia.functional.xef.llm.assistants.RunDelta +import com.xebia.functional.xef.llm.assistants.Tool +import kotlinx.serialization.Serializable + +@Description("Natural numbers") +enum class NaturalWithDescriptions { + @Description("If the number is positive.") POSITIVE, + @Description("If the number is negative.") NEGATIVE +} + +@Serializable +data class SumInputWithDescription( + @Description("Left operand") val left: Int, + @Description("Right operand") val right: Int, + val natural: NaturalWithDescriptions +) + +class SumToolWithDescription : Tool { + override suspend fun invoke(input: SumInputWithDescription): Int { + return input.left + input.right + } +} + +suspend fun main() { + val toolConfig = Tool.toolOf(SumToolWithDescription()).functionObject + println(toolConfig.parameters) +} + +private suspend fun runAssistantAndDisplayResults(thread: AssistantThread, assistant: Assistant) { + thread.run(assistant).collect(RunDelta::printEvent) +} From 5fb134f50dc5fc03ff778abd535389ddb54c4389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Raja=20Mart=C3=ADnez?= Date: Thu, 2 May 2024 23:23:46 +0200 Subject: [PATCH 07/17] Update README with instruction to build locally (#725) * Add README instructions for building Xef * Include reasons why build may fail if you don't have docker --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 2b88706ad..64cdaa6e1 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,17 @@ In [this](https://xef.ai/learn/quickstart/) small introduction we look at the ma ## 🚀 Examples You can also have a look at the [examples](https://github.com/xebia-functional/xef/tree/main/examples/src/main/kotlin/com/xebia/functional/xef/conversation) to have a feeling of how using the library looks like. + +## 🚧 Local Development + +To build the project locally, you can use the following commands: + +```shell +./gradlew downloadOpenAIAPI +./gradlew openaiClientGenerate +./gradlew build +``` + +The server and postgres tests may fail if you don't have [Docker](https://www.docker.com/) installed. +The server and postgres related tests depend on [Testcontainers](https://testcontainers.com/), which in turn depends on Docker. + From 1aef2237fa5f10748bcabd8947adc9cb12b2532f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Carlos=20Monta=C3=B1ez?= Date: Mon, 6 May 2024 16:31:39 +0200 Subject: [PATCH 08/17] fixed error in enum description (#733) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Carlos Montañez --- .../xef/llm/models/functions/JsonSchema.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt index e7211c2aa..6fd44efd1 100644 --- a/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt +++ b/core/src/commonMain/kotlin/com/xebia/functional/xef/llm/models/functions/JsonSchema.kt @@ -327,15 +327,21 @@ private fun JsonObjectBuilder.applyJsonSchemaDefaults( if (descriptor.kind == SerialKind.ENUM) { this["enum"] = descriptor.elementNames descriptor.elementNames - .mapIndexed { index, name -> - "$name (${descriptor.getElementAnnotations(index).lastOfInstance()?.value})" + .mapIndexedNotNull { index, name -> + val enumDescription = + descriptor.getElementAnnotations(index).lastOfInstance()?.value + if (enumDescription != null) { + "$name ($enumDescription)" + } else { + null + } } .joinToString("\n - ") } else null if (annotations.isNotEmpty()) { val description = annotations.filterIsInstance().firstOrNull()?.value - if (additionalEnumDescription != null) { + if (!additionalEnumDescription.isNullOrEmpty()) { this["description"] = "$description\n - $additionalEnumDescription" } else { this["description"] = description From dd7aa9b688273b2c9f6413999779e32f3099eaac Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Mon, 13 May 2024 13:36:18 +0200 Subject: [PATCH 09/17] wip-assistant-form --- .../src/main/resources/documents/ghibli.json | 279 +++++++ .../xef/server/http/routes/AssistantRoutes.kt | 87 ++ .../xef/server/http/routes/XefRoutes.kt | 1 + .../Pages/Assistants/Assistants.tsx | 777 ++++++++++++------ 4 files changed, 915 insertions(+), 229 deletions(-) create mode 100644 examples/src/main/resources/documents/ghibli.json create mode 100644 server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt diff --git a/examples/src/main/resources/documents/ghibli.json b/examples/src/main/resources/documents/ghibli.json new file mode 100644 index 000000000..6a9b0353c --- /dev/null +++ b/examples/src/main/resources/documents/ghibli.json @@ -0,0 +1,279 @@ +[ { +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/npOnzAbLh6VOIu3naU5QaEcTepo.jpg" , +"movie_banner": "https://image.tmdb.org/t/p/w533_and_h300_bestv2/3cyjYtLWCBE1uvWINHFsFnE8LUK.jpg" , +"description": "The orphan Sheeta inherited a mysterious crystal that links her to the mythical sky-kingdom of Laputa. With the help of resourceful Pazu and a rollicking band of sky pirates, she makes her way to the ruins of the once-great civilization. Sheeta and Pazu must outwit the evil Muska, who plans to use Laputa's science to make himself ruler of the world.", +"director": "Hayao Miyazaki", "producer": "Isao Takahata", "release_date": "1986", "running_time": "124", "rt_score": "95", +"people": [ "https://ghibliapi.vercel.app/people/598f7048-74ff-41e0-92ef-87dc1ad980a9", "https://ghibliapi.vercel.app/people/fe93adf2-2f3a-4ec4-9f68-5422f1b87c01", "https://ghibliapi.vercel.app/people/3bc0b41e-3569-4d20-ae73-2da329bf0786", "https://ghibliapi.vercel.app/people/40c005ce-3725-4f15-8409-3e1b1b14b583", "https://ghibliapi.vercel.app/people/5c83c12a-62d5-4e92-8672-33ac76ae1fa0", "https://ghibliapi.vercel.app/people/e08880d0-6938-44f3-b179-81947e7873fc", "https://ghibliapi.vercel.app/people/2a1dad70-802a-459d-8cc2-4ebd8821248b" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/4e09b023-f650-4747-9ab9-eacf14540cfb" ], +"url": "https://ghibliapi.vercel.app/films/2baf70d1-42bb-4437-b551-e5fed5a87abe" }, +{
"id": "12cfb892-aac0-4c5b-94af-521852e46d6a", "title": "Grave of the Fireflies",
"original_title": "火垂るの墓", "original_title_romanised": "Hotaru no haka", +"id": "2baf70d1-42bb-4437-b551-e5fed5a87abe", "title": "Castle in the Sky",
"original_title": "天空の城ラピュタ", "original_title_romanised": "Tenkū no shiro Rapyuta", "image": +"image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/qG3RYlIVpTYclR9TYIsy8p7m7AT.jpg", +"movie_banner": "https://image.tmdb.org/t/p/original/vkZSd0Lp8iCVBGpFH9L7LzLusjS.jpg", +"description": "In the latter part of World War II, a boy and his sister, orphaned when their mother is killed in the firebombing of Tokyo, are left to survive on their own in what remains of civilian life in Japan. The plot follows this boy and his sister as they do their best to survive in the Japanese countryside, battling hunger, prejudice, and pride in their own quiet, personal battle.", +"director": "Isao Takahata", "producer": "Toru Hara", "release_date": "1988", "running_time": "89", "rt_score": "97", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/12cfb892-aac0-4c5b-94af-521852e46d6a" }, +{
"id": "58611129-2dbc-4a81-a72f-77ddfc1b1b49", "title": "My Neighbor Totoro",
"original_title": "となりのトトロ", "original_title_romanised": "Tonari no Totoro", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/rtGDOeG9LzoerkDGZF9dnVeLppL.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/etqr6fOOCXQOgwrQXaKwenTSuzx.jpg", "description": "Two sisters move to the country with their father in order to be closer to +their hospitalized mother, and discover the surrounding trees are inhabited by Totoros, magical spirits of the forest. When the youngest runs away from home, the older sister seeks help from the spirits to find her.", +"director": "Hayao Miyazaki", "producer": "Hayao Miyazaki", "release_date": "1988", "running_time": "86", "rt_score": "93", +"people": [ "https://ghibliapi.vercel.app/people/986faac6-67e3-4fb8-a9ee-bad077c2e7fe", +"https://ghibliapi.vercel.app/people/d5df3c04-f355-4038-833c-83bd3502b6b9", "https://ghibliapi.vercel.app/people/3031caa8-eb1a-41c6-ab93-dd091b541e11", "https://ghibliapi.vercel.app/people/87b68b97-3774-495b-bf80-495a5f3e672d", "https://ghibliapi.vercel.app/people/d39deecb-2bd0-4770-8b45-485f26e1381f", "https://ghibliapi.vercel.app/people/591524bc-04fe-4e60-8d61-2425e42ffb2a", "https://ghibliapi.vercel.app/people/c491755a-407d-4d6e-b58a-240ec78b5061", "https://ghibliapi.vercel.app/people/f467e18e-3694-409f-bdb3-be891ade1106", "https://ghibliapi.vercel.app/people/08ffbce4-7f94-476a-95bc-76d3c3969c19", "https://ghibliapi.vercel.app/people/0f8ef701-b4c7-4f15-bd15-368c7fe38d0a" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2", "https://ghibliapi.vercel.app/species/603428ba-8a86-4b0b-a9f1-65df6abef3d3", "https://ghibliapi.vercel.app/species/74b7f547-1577-4430-806c-c358c8b6bcf5" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/58611129-2dbc-4a81-a72f-77ddfc1b1b49" }, +{
"id": "ea660b10-85c4-4ae3-8a5f-41cea3648e3e", "title": "Kiki's Delivery Service",
"original_title": "魔女の宅急便", "original_title_romanised": "Majo no takkyūbin", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/7nO5DUMnGUuXrA4r2h6ESOKQRrx.jp g", +"movie_banner": "https://image.tmdb.org/t/p/original/h5pAEVma835u8xoE60kmLVopLct.jpg", +"description": "A young witch, on her mandatory year of independent life, finds fitting into a new community difficult while she supports herself by running an air courier service.", +"director": "Hayao Miyazaki", "producer": "Hayao Miyazaki", "release_date": "1989", "running_time": "102", "rt_score": "96", +"people": [ "https://ghibliapi.vercel.app/people/2409052a-9029-4e8d-bfaf-70fd82c8e48d", "https://ghibliapi.vercel.app/people/7151abc6-1a9e-4e6a-9711-ddb50ea572ec", "https://ghibliapi.vercel.app/people/1c1a8054-3a34-4185-bfcf-e8011506f09a", "https://ghibliapi.vercel.app/people/bc838920-7849-43ea-bfb8-7d5e98dc20b6", "https://ghibliapi.vercel.app/people/33f5fea9-c21b-490b-90e0-c4051c372826", "https://ghibliapi.vercel.app/people/d1de1c0e-3fcd-4cef-94eb-bb95cc2314aa" +], +"species": [ "https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2", "https://ghibliapi.vercel.app/species/603428ba-8a86-4b0b-a9f1-65df6abef3d3" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/ea660b10-85c4-4ae3-8a5f-41cea3648e3e" }, +{
"id": "4e236f34-b981-41c3-8c65-f8c9000b94e7", "title": "Only Yesterday",
"original_title": "おもひでぽろぽろ", "original_title_romanised": "Omoide poro poro", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/xjJU6rwzLX7Jk8HFQfVW6H5guMC.jpg" , +"movie_banner": "https://image.tmdb.org/t/p/w533_and_h300_bestv2/isCrlWWI4JrdLKAUAwFb5cjAsH4.jpg", +"description": "It’s 1982, and Taeko is 27 years old, unmarried, and has lived her whole life in Tokyo. She decides to visit her family in the countryside, and as the train travels through the night, memories flood back of her younger years: the first immature stirrings of romance, the onset of puberty, and the frustrations of math and boys. At the station she is met by young farmer Toshio, and the encounters with him begin to reconnect her to forgotten longings. In lyrical switches between the present and the past, Taeko contemplates the arc of her life, and wonders if she has been true to the dreams of her childhood self.", +"director": "Isao Takahata", "producer": "Toshio Suzuki", "release_date": "1991", "running_time": "118", "rt_score": "100", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/4e236f34-b981-41c3-8c65-f8c9000b94e7" }, +{
"id": "ebbb6b7c-945c-41ee-a792-de0e43191bd8", "title": "Porco Rosso",
"original_title": "紅の豚", "original_title_romanised": "Kurenai no buta", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/byKAndF6KQSDpGxp1mTr23jPbYp.jpg" , +"movie_banner": "https://image.tmdb.org/t/p/original/nAeCzilMRXvGaxiCpv63ZRVRVgh.jpg", +"description": "Porco Rosso, known in Japan as Crimson Pig (Kurenai no Buta) is the sixth animated film by Hayao Miyazaki and released in 1992. You're introduced to an Italian World War I fighter ace, now living as a freelance bounty hunter chasing 'air pirates' in the Adriatic Sea. He has been given a curse that changed his head to that of a pig. Once called Marco Pagot, he is now known to the world as 'Porco Rosso', Italian for 'Red Pig.'", +"director": "Hayao Miyazaki", "producer": "Toshio Suzuki", "release_date": "1992", "running_time": "93", "rt_score": "94", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/ebbb6b7c-945c-41ee-a792-de0e43191bd8" }, +{
"id": "1b67aa9a-2e4a-45af-ac98-64d6ad15b16c",
"title": "Pom Poko",
"original_title": "平成狸合戦ぽんぽこ", "original_title_romanised": "Heisei tanuki gassen Ponpoko", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/kowo9E1e1JcWLXj9cCvAOFZcy5n.jpg" , +"movie_banner": "https://image.tmdb.org/t/p/original/jScPd0u0jeo66l8gwDl7W9hDUnM.jpg", +"description": "As the human city development encroaches on the raccoon population's forest and meadow habitat, the raccoons find themselves faced with the very real possibility of extinction. In response, the raccoons engage in a desperate struggle to stop the construction and preserve their home.", +"director": "Isao Takahata", "producer": "Toshio Suzuki", "release_date": "1994", "running_time": "119", "rt_score": "78", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/1b67aa9a-2e4a-45af-ac98-64d6ad15b16c" }, +{
"id": "ff24da26-a969-4f0e-ba1e-a122ead6c6e3", "title": "Whisper of the Heart",
"original_title": "耳をすませば", "original_title_romanised": "Mimi wo sumaseba", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/5E3Hvbu0bg38ouYf6chGftVGqZ7.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/fRtaDgmj0CirvqFUG1XN48BDY1l.jpg",
"description": "Shizuku lives a simple life, dominated by her love for stories and writing. +One day she notices that all the library books she has have been previously checked out by the same person: 'Seiji Amasawa'. Curious as to who he is, Shizuku meets a boy her age whom she finds infuriating, but discovers to her shock that he is her 'Prince of Books'. As she grows closer to him, she realises that he merely read all those books to bring himself closer to her. The boy Seiji aspires to be a violin maker in Italy, and it is his dreams that make Shizuku realise that she has no clear path for her life. Knowing that her strength lies in writing, she tests her talents by writing a story about Baron, a cat statuette belonging to Seiji's grandfather.", +"director": "Yoshifumi Kondō", "producer": "Toshio Suzuki", "release_date": "1995", "running_time": "111", "rt_score": "91", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/ff24da26-a969-4f0e-ba1e-a122ead6c6e3" }, +{
"id": "0440483e-ca0e-4120-8c50-4c8cd9b965d6", "title": "Princess Mononoke",
"original_title": "もののけ姫", "original_title_romanised": "Mononoke hime", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/jHWmNr7m544fJ8eItsfNk8fs2Ed.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/6pTqSq0zYIWCsucJys8q5L92kUY.jpg",
"description": "Ashitaka, a prince of the disappearing Ainu tribe, is cursed by a demonized +boar god and must journey to the west to find a cure. Along the way, he encounters San, a young human woman fighting to protect the forest, and Lady Eboshi, who is trying to destroy it. Ashitaka must find a way to bring balance to this conflict.", +"director": "Hayao Miyazaki", "producer": "Toshio Suzuki", "release_date": "1997", "running_time": "134", "rt_score": "92", +"people": [ "https://ghibliapi.vercel.app/people/ba924631-068e-4436-b6de-f3283fa848f0", "https://ghibliapi.vercel.app/people/ebe40383-aad2-4208-90ab-698f00c581ab", "https://ghibliapi.vercel.app/people/030555b3-4c92-4fce-93fb-e70c3ae3df8b", "https://ghibliapi.vercel.app/people/ca568e87-4ce2-4afa-a6c5-51f4ae80a60b", "https://ghibliapi.vercel.app/people/e9356bb5-4d4a-4c93-aadc-c83e514bffe3", "https://ghibliapi.vercel.app/people/34277bec-7401-43fa-a00a-5aee64b45b08", "https://ghibliapi.vercel.app/people/91939012-90b9-46e5-a649-96b898073c82", "https://ghibliapi.vercel.app/people/20e3bd33-b35d-41e6-83a4-57ca7f028d38", "https://ghibliapi.vercel.app/people/8bccdc78-545b-49f4-a4c8-756163a38c91", "https://ghibliapi.vercel.app/people/116bfe1b-3ba8-4fa0-8f72-88537a493cb9" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2", "https://ghibliapi.vercel.app/species/6bc92fdd-b0f4-4286-ad71-1f99fb4a0d1e", "https://ghibliapi.vercel.app/species/f25fa661-3073-414d-968a-ab062e3065f7" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/0440483e-ca0e-4120-8c50-4c8cd9b965d6" }, +{
"id": "45204234-adfd-45cb-a505-a8e7a676b114",
"title": "My Neighbors the Yamadas",
"original_title": "ホーホケキョ となりの山田くん", "original_title_romanised": "Hōhokekyo tonari no Yamada-kun", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/wTGuHmMIBBgKakY80J1D52VvQKI.jp g", +"movie_banner": "https://image.tmdb.org/t/p/original/nDOsicEg4RHDq0t23JKGSb58z6u.jpg", +"description": "The Yamadas are a typical middle class Japanese family in urban Tokyo and this film shows us a variety of episodes of their lives. With tales that range from the humourous to the heartbreaking, we see this family cope with life's little conflicts, problems and joys in their own way.", +"director": "Isao Takahata", "producer": "Toshio Suzuki", "release_date": "1999", "running_time": "104", "rt_score": "75", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/45204234-adfd-45cb-a505-a8e7a676b114" }, +{
"id": "dc2e6bd1-8156-4886-adff-b39e6043af0c",
"title": "Spirited Away",
"original_title": "千と千尋の神隠し", "original_title_romanised": "Sen to Chihiro no kamikakushi", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/39wmItIWsg5sZMyRUHLkWBcuVCM.jp g", +"movie_banner": "https://image.tmdb.org/t/p/original/bSXfU4dwZyBA1vMmXvejdRXBvuF.jpg", +"description": "Spirited Away is an Oscar winning Japanese animated film about a ten year old girl who wanders away from her parents along a path that leads to a world ruled by +strange and unusual monster-like animals. Her parents have been changed into pigs along with others inside a bathhouse full of these creatures. Will she ever see the world how it once was?", +"director": "Hayao Miyazaki", "producer": "Toshio Suzuki", "release_date": "2001", "running_time": "124", "rt_score": "97", +"people": [ "https://ghibliapi.vercel.app/people/8228751c-bdc1-4b8d-a6eb-ca0eb909568f" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/dc2e6bd1-8156-4886-adff-b39e6043af0c" }, +{
"id": "90b72513-afd4-4570-84de-a56c312fdf81", "title": "The Cat Returns",
"original_title": "猫の恩返し", "original_title_romanised": "Neko no ongaeshi", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/avPMO5cnaGHgLaNiAIhy33WoQLm.jp g", +"movie_banner": "https://image.tmdb.org/t/p/original/d4BTZvckFTthyhGX27LZnWxl0tl.jpg", +"description": "Haru, a schoolgirl bored by her ordinary routine, saves the life of an unusual cat and suddenly her world is transformed beyond anything she ever imagined. The Cat King rewards her good deed with a flurry of presents, including a very shocking proposal of marriage to his son! Haru embarks on an unexpected journey to the Kingdom of Cats where her eyes are opened to a whole other world.", +"director": "Hiroyuki Morita", "producer": "Toshio Suzuki", "release_date": "2002", "running_time": "75", "rt_score": "89", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/90b72513-afd4-4570-84de-a56c312fdf81" }, +{
"id": "cd3d059c-09f4-4ff3-8d63-bc765a5184fa", "title": "Howl's Moving Castle",
"original_title": "ハウルの動く城", "original_title_romanised": "Hauru no ugoku shiro", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/TkTPELv4kC3u1lkloush8skOjE.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/hjlvbMKhQm7N8tYynr8yQ8GBmqe.jpg",
"description": "When Sophie, a shy young woman, is cursed with an old body by a spiteful +witch, her only chance of breaking the spell lies with a self-indulgent yet insecure young wizard and his companions in his legged, walking home.", +"director": "Hayao Miyazaki", "producer": "Toshio Suzuki", "release_date": "2004", "running_time": "119", "rt_score": "87", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/cd3d059c-09f4-4ff3-8d63-bc765a5184fa" }, +{
"id": "112c1e67-726f-40b1-ac17-6974127bb9b9", "title": "Tales from Earthsea",
"original_title": "ゲド戦記", "original_title_romanised": "Gedo senki", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/67yYwCPq7NbxSF6BIIXCMD34sY0.jpg ", +"movie_banner": "https://image.tmdb.org/t/p/original/j276noIGGmfi66EnCfewsL2OVTX.jpg", +"description": "Something bizarre has come over the land. The kingdom is deteriorating. People are beginning to act strange... What's even more strange is that people are beginning to see dragons, which shouldn't enter the world of humans. Due to all these bizarre events, Ged, a wandering wizard, is investigating the cause. During his journey, he meets Prince Arren, a young distraught teenage boy. While Arren may look like a shy young teen, he has a severe dark side, which grants him strength, hatred, ruthlessness and has no mercy, especially when it comes to protecting Teru. For the witch Kumo this is a perfect opportunity. She can use the boy's 'fears' against the very one who would help him, Ged.", +"director": "Gorō Miyazaki", "producer": "Toshio Suzuki", "release_date": "2006", "running_time": "116", "rt_score": "41", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/112c1e67-726f-40b1-ac17-6974127bb9b9" }, +{
"id": "758bf02e-3122-46e0-884e-67cf83df1786", "title": "Ponyo",
"original_title": "崖の上のポニョ", "original_title_romanised": "Gake no ue no Ponyo", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/mikKSEdk5kLhflWXbp4S5mmHsDo.jpg ", +"movie_banner": "https://image.tmdb.org/t/p/original/6a1qZ1qat26mAIK3Lq8iYdGpyHm.jpg", +"description": "The son of a sailor, 5-year old Sosuke lives a quiet life on an oceanside cliff with his mother Lisa. One fateful day, he finds a beautiful goldfish trapped in a bottle on the beach and upon rescuing her, names her Ponyo. But she is no ordinary goldfish. The daughter of a masterful wizard and a sea goddess, Ponyo uses her father's magic to transform herself into a young girl and quickly falls in love with Sosuke, but the use of such powerful sorcery causes a dangerous imbalance in the world. As the moon steadily draws nearer to the earth and Ponyo's father sends the ocean's mighty waves to find his daughter, the two children embark on an adventure of a lifetime to save the world and fulfill Ponyo's dreams of becoming human.", +"director": "Hayao Miyazaki", "producer": "Toshio Suzuki", +"release_date": "2008", "running_time": "100", "rt_score": "92", "people": [ +"https://ghibliapi.vercel.app/people/" ], +"species": [ "https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/758bf02e-3122-46e0-884e-67cf83df1786" }, +{
"id": "2de9426b-914a-4a06-a3a0-5e6d9d3886f6", "title": "Arrietty",
"original_title": "借りぐらしのアリエッティ", "original_title_romanised": "Karigurashi no Arietti", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/oc2OB2KDmSRDMelKEAA1n4YRQL0.j pg", +"movie_banner": "https://image.tmdb.org/t/p/original/7Z7WVzJsSReG8B0CaPk0bvWD7tK.jpg", +"description": "14-year-old Arrietty and the rest of the Clock family live in peaceful anonymity as they make their own home from items 'borrowed' from the house's human inhabitants. However, life changes for the Clocks when a human boy discovers Arrietty.", +"director": "Hiromasa Yonebayashi", "producer": "Toshio Suzuki", "release_date": "2010", "running_time": "94", +"rt_score": "95", "people": [ +"https://ghibliapi.vercel.app/people/" ], +"species": [ "https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +],
"url": "https://ghibliapi.vercel.app/films/2de9426b-914a-4a06-a3a0-5e6d9d3886f6" +}, { +"id": "45db04e4-304a-4933-9823-33f389e8d74d", "title": "From Up on Poppy Hill",
"original_title": "コクリコ坂から", "original_title_romanised": "Kokuriko zaka kara", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/rRLYX4RZIyloHSJwvZKAhphAjiB.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/xtPBZYaWQMQxRpy7mkdk5n1bTxs.jpg", "description": "The story is set in 1963 in Yokohama. Kokuriko Manor sits on a hill +overlooking the harbour. A 16 year-old girl, Umi, lives in that house. Every morning she raises a signal flag facing the sea. The flag means “I pray for safe voyages”. A 17 year-old boy, Shun, always sees this flag from the sea as he rides a tugboat to school. Gradually the pair are drawn to each other but they are faced with a sudden trial. Even so, they keep going without running from facing the hardships of reality.", +"director": "Gorō Miyazaki", "producer": "Toshio Suzuki", "release_date": "2011", "running_time": "91", "rt_score": "83", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], "vehicles": [ +"https://ghibliapi.vercel.app/vehicles/" ], +"url": "https://ghibliapi.vercel.app/films/45db04e4-304a-4933-9823-33f389e8d74d" }, +{
"id": "67405111-37a5-438f-81cc-4666af60c800", "title": "The Wind Rises",
"original_title": "風立ちぬ", "original_title_romanised": "Kaze tachinu", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/jfwSexzlIzaOgxP9A8bTA6t8YYb.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/stM3jlD4nSJhlvR2DE7XnB0eN25.jpg",
"description": "A lifelong love of flight inspires Japanese aviation engineer Jiro Horikoshi, +whose storied career includes the creation of the A-6M World War II fighter plane.", "director": "Hayao Miyazaki",
"producer": "Toshio Suzuki", +"release_date": "2013", "running_time": "126", "rt_score": "89", "people": [ +"https://ghibliapi.vercel.app/people/" ], +"species": [ "https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/67405111-37a5-438f-81cc-4666af60c800" }, +{
"id": "578ae244-7750-4d9f-867b-f3cd3d6fecf4",
"title": "The Tale of the Princess Kaguya",
"original_title": "かぐや姫の物語", "original_title_romanised": "Kaguya-Hime no Monogatari", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/mWRQNlWXYYfd2z4FRm99MsgHgiA.j pg", +"movie_banner": "https://image.tmdb.org/t/p/original/lMaWlYThCSnsmW3usxWTpSuyZp1.jpg", +"description": "A bamboo cutter named Sanuki no Miyatsuko discovers a miniature girl inside a glowing bamboo shoot. Believing her to be a divine presence, he and his wife decide to raise her as their own, calling her 'Princess'.", +"director": "Isao Takahata", "producer": "Yoshiaki Nishimura", "release_date": "2013", "running_time": "137",
"rt_score": "100",
"people": [ +"https://ghibliapi.vercel.app/people/" ], +"species": [ "https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +],
"url": "https://ghibliapi.vercel.app/films/578ae244-7750-4d9f-867b-f3cd3d6fecf4" +}, { +"id": "5fdfb320-2a02-49a7-94ff-5ca418cae602", "title": "When Marnie Was There", "original_title": "思い出のマーニー", "original_title_romanised": "Omoide no Marnie", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/vug1dvDI1tSa60Z8qjCuUE7ntkO.jpg", "movie_banner": +"https://image.tmdb.org/t/p/original/axUX7urQDwCGQ9qbgh2Yys7qY9J.jpg", "description": "The film follows Anna Sasaki living with her relatives in the seaside town. +Anna comes across a nearby abandoned mansion, where she meets Marnie, a mysterious girl who asks her to promise to keep their secrets from everyone. As the summer progresses, Anna spends more time with Marnie, and eventually Anna learns the truth about her family and foster care.", +"director": "Hiromasa Yonebayashi", "producer": "Yoshiaki Nishimura", "release_date": "2014", "running_time": "103", +"rt_score": "92", "people": [ +"https://ghibliapi.vercel.app/people/" ], +"species": [ "https://ghibliapi.vercel.app/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/5fdfb320-2a02-49a7-94ff-5ca418cae602" }, +{
"id": "d868e6ec-c44a-405b-8fa6-f7f0f8cfb500",
"title": "The Red Turtle",
"original_title": "レッドタートル ある島の物語", "original_title_romanised": "Reddotātoru aru shima no monogatari", "image": +"https://image.tmdb.org/t/p/w600_and_h900_bestv2/wOBU3SLjQ9358Km9YWYasPZyebp.jp g", +"movie_banner": "https://image.tmdb.org/t/p/original/kjXdW5H3myRBmTMYgKayjphr2FA.jpg", +"description": "A man set adrift by a storm wakes up on a beach. He discovers that he is on a deserted island with plenty of fresh water, fruit and a dense bamboo forest. He builds a raft from bamboo and attempts to sail away, but his raft is destroyed by an unseen monster in the sea, forcing him back to the island. He tries again with another, larger raft, but is again +foiled by the creature. A third attempt again ends with the raft destroyed, but this time he is confronted by a giant red turtle, which stares at him, and forces him back to the island.", +"director": "Michaël Dudok de Wit", +"producer": "Toshio Suzuki, Isao Takahata, Vincent Maraval, Pascal Caucheteux, Grégoire Sorlat", +"release_date": "2016", "running_time": "80", "rt_score": "93", "people": [ +"https://ghibliapi.vercel.app/people/" ], +"species": [ "https://ghibliapi.vercel.app/species/" +], "locations": [ +"https://ghibliapi.vercel.app/locations/" ], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/d868e6ec-c44a-405b-8fa6-f7f0f8cfb500" }, +{
"id": "790e0028-a31c-4626-a694-86b7a8cada40", "title": "Earwig and the Witch",
"original_title": "アーヤと魔女", "original_title_romanised": "Āya to Majo", "image": +"https://www.themoviedb.org/t/p/w600_and_h900_bestv2/sJhFtY3eHuvvACaPpxpzdCLQqp Q.jpg", +"movie_banner": "https://www.themoviedb.org/t/p/original/qMxpGzmmnY1jLd4p7EhhoW43wWF.jpg", +"description": "An orphan girl, Earwig, is adopted by a witch and comes home to a spooky house filled with mystery and magic.", +"director": "Gorō Miyazaki", "producer": "Toshio Suzuki", "release_date": "2021", "running_time": "82", "rt_score": "30", +"people": [ "https://ghibliapi.vercel.app/people/" +], "species": [ +"https://ghibliapi.vercel.app/species/" ], +"locations": [ "https://ghibliapi.vercel.app/locations/" +], +"vehicles": [ "https://ghibliapi.vercel.app/vehicles/" +], +"url": "https://ghibliapi.vercel.app/films/790e0028-a31c-4626-a694-86b7a8cada40" } +] diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt new file mode 100644 index 000000000..02b64279d --- /dev/null +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -0,0 +1,87 @@ +package com.xebia.functional.xef.server.http.routes + +import com.xebia.functional.openai.generated.model.AssistantObject +import com.xebia.functional.openai.generated.model.CreateAssistantRequest +import com.xebia.functional.openai.generated.model.ListAssistantsResponse +import com.xebia.functional.xef.Config +import com.xebia.functional.xef.OpenAI +import com.xebia.functional.xef.server.models.Token +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Routing.assistantRoutes() { + authenticate("auth-bearer") { + post("/v1/settings/assistants") { + try { + val contentType = call.request.contentType() + if (contentType == ContentType.Application.Json) { + val request = call.receive() + val token = call.getToken() + val response = createAssistant(token, request) + call.respond(status = HttpStatusCode.Created, response) + } else { + call.respond( + HttpStatusCode.UnsupportedMediaType, + "Unsupported content type: $contentType" + ) + } + } catch (e: Exception) { + val trace = e.stackTraceToString() + call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") + } + } + + // put("/v1/settings/assistants/{id}") { + // val request = Json.decodeFromString(call.receive()) + // val token = call.getToken() + // val id = call.getId() + // val response = updateAssistant(token, request, id) + // call.respond(status = HttpStatusCode.NoContent, response) + // } + // get("/v1/settings/assistants") { + // val token = call.getToken() + // val response = ListAssistantsResponse("list", emptyList(), null, null, false) + // val assistantResponse = listAssistants(token, response) + // call.respond(assistantResponse) + // } + // delete("/v1/settings/assistants/{id}") { + // val token = call.getToken() + // val id = call.parameters["id"]?.toIntOrNull() + // if (id == null) { + // call.respond(HttpStatusCode.BadRequest, "Invalid assistant id") + // return@delete + // } + // val response = deleteAssistant(token, id) + // call.respond(status = HttpStatusCode.NoContent, response) + // } + } +} + +suspend fun createAssistant(token: Token, request: CreateAssistantRequest): AssistantObject { + val openAIConfig = Config(token = token.value) + val openAI = OpenAI(openAIConfig) + val assistants = openAI.assistants + val assistantObject = assistants.createAssistant(request) + return assistantObject +} + +// suspend fun updateAssistant(token: String, request: AssistantRequest, id: Int): String { +// // Implement the logic for updating an assistant in OpenAI here +// } + +suspend fun listAssistants(token: Token, response: ListAssistantsResponse): ListAssistantsResponse { + val openAIConfig = Config(token = token.value) + val openAI = OpenAI(openAIConfig) + val assistants = openAI.assistants + val listAssistants = assistants.listAssistants() + + return listAssistants +} + +/*suspend fun deleteAssistant(token: String, id: Int): String { + // Implement the logic for deleting an assistant in OpenAI here +}*/ diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt index 47b15f346..a4dcbfbb6 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt @@ -12,4 +12,5 @@ fun Routing.xefRoutes(logger: KLogger) { organizationRoutes(OrganizationRepositoryService(logger)) projectsRoutes(ProjectRepositoryService(logger)) tokensRoutes(TokenRepositoryService(logger)) + assistantRoutes() } diff --git a/server/web/src/components/Pages/Assistants/Assistants.tsx b/server/web/src/components/Pages/Assistants/Assistants.tsx index d6e58d6da..83e2a61b9 100644 --- a/server/web/src/components/Pages/Assistants/Assistants.tsx +++ b/server/web/src/components/Pages/Assistants/Assistants.tsx @@ -2,39 +2,33 @@ import { useContext, useEffect, useState, ChangeEvent } from "react"; import { useAuth } from "@/state/Auth"; import { LoadingContext } from "@/state/Loading"; -import { - getAssistants, - postAssistant, - putAssistant, - deleteAssistant, -} from "@/utils/api/assistants"; import { Alert, - Box, - Button, - Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - FormControlLabel, - FormGroup, - MenuItem, - Grid, - Divider, - Paper, - Switch, - Snackbar, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Typography, - Slider + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + FormControlLabel, + FormGroup, + Grid, + Divider, + Paper, + Snackbar, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Slider, + MenuItem, + Switch } from "@mui/material"; type Assistant = { @@ -63,54 +57,16 @@ export function Assistants() { const [JsonObjectEnabled, setJsonObjectEnabled] = useState(false); const [temperature, setTemperature] = useState(1); const [topP, setTopP] = useState(1); + const [assistantCreated, setAssistantCreated] = useState(false); - async function loadAssistants() { - setLoading(true); - try { - const response = await getAssistants(auth.authToken); - setAssistants(response); - } catch (error) { - console.error('Error fetching assistants:', error); - setShowAlert('Failed to load assistants.'); - } - setLoading(false); - } - - useEffect(() => { - loadAssistants(); - }, []); - - const models = [ - { - value: 'gpt-4-turbo', - label: 'gpt-4-turbo', - }, - { - value: 'gpt-4', - label: 'gpt-4', - }, - { - value: 'gpt-3.5-turbo-16k', - label: 'gpt-3.5-turbo-16k', - }, - { - value: 'gpt-3.5-turbo-0125', - label: 'gpt-3.5-turbo-0125', - }, - { - value: 'gpt-3.5-turbo', - label: 'gpt-3.5-turbo', - }, - { - value: 'gpt-3.5-turbo', - label: 'gpt-3.5-turbo', - }, - ]; + const [fileSearchSelectedFile, fileSearchSetSelectedFile] = useState([]); + const [fileSearchDialogOpen, setFileSearchDialogOpen] = useState(false); + const [codeInterpreterSelectedFile, codeInterpreterSetSelectedFile] = useState([]); + const [codeInterpreterDialogOpen, setCodeInterpreterDialogOpen] = useState(false); - const handleCreateAssistant = async () => { - // Aquí se incluirá la lógica para crear un nuevo asistente - // Por ejemplo: await postAssistant(authToken, { name: selectedAssistant.name }); - }; + const [fileSearchFiles, setFileSearchFiles] = useState<{ name: string; size: number; uploaded: string }[]>([]); + const [attachedFilesFileSearch, setAttachedFilesFileSearch] = useState<{ name: string; size: string; uploaded: string }[]>([]); + const [attachedFilesCodeInterpreter, setAttachedFilesCodeInterpreter] = useState<{ name: string; size: string; uploaded: string }[]>([]); const handleFileSearchChange = (event: ChangeEvent) => { setFileSearchEnabled(event.target.checked); @@ -121,165 +77,528 @@ export function Assistants() { }; const handleJsonObjectChange = (event: ChangeEvent) => { - setJsonObjectEnabled(event.target.checked); - }; + setJsonObjectEnabled(event.target.checked); + }; const handleTemperatureChange = (event: Event, newValue: number | number[]) => { setTemperature(newValue as number); }; + const handleTopPChange = (event: Event, newValue: number | number[]) => { - setTopP(newValue as number); - }; - const handleFilesButtonClick = (toolName: String)=> { - console.log(`Clicked on ${toolName} button`); + setTopP(newValue as number); + }; + + const handleTemperatureInputChange = (event: React.ChangeEvent) => { + const value = parseFloat(event.target.value); + if (!isNaN(value)) { + setTemperature(value); + } + }; + + const handleTopPInputChange = (event: React.ChangeEvent) => { + const value = parseFloat(event.target.value); + if (!isNaN(value)) { + setTopP(value); + } + }; + + const handleFileSearchDialogClose = () => { + setFileSearchDialogOpen(false); + setAttachedFilesFileSearch(fileSearchSelectedFile); + }; + + const handleCodeInterpreterDialogClose = () => { + setCodeInterpreterDialogOpen(false); + setAttachedFilesCodeInterpreter(codeInterpreterSelectedFile); + }; + + const handleFileSearchButtonClick = () => { + setFileSearchDialogOpen(true); + }; + + const handleCodeInterpreterButtonClick = () => { + setCodeInterpreterDialogOpen(true); + }; + + const handleFileSearchInputChange = (event: ChangeEvent) => { + const files = Array.from(event.target.files); + const newFiles = files.map((file) => ({ + name: file.name, + size: `${Math.round(file.size / 1024)} KB`, + uploaded: new Date().toLocaleString() + })); + fileSearchSetSelectedFile((prevFiles) => [...prevFiles, ...newFiles]); + }; + + const handleCodeInterpreterInputChange = (event: ChangeEvent) => { + const files = Array.from(event.target.files); + const newFiles = files.map((file) => ({ + name: file.name, + size: `${Math.round(file.size / 1024)} KB`, + uploaded: new Date().toLocaleString() + })); + codeInterpreterSetSelectedFile((prevFiles) => [...prevFiles, ...newFiles]); + }; + + const handleDeleteFile = (index: number) => { + const updatedFiles = fileSearchSelectedFile.filter((_, i) => i !== index); + fileSearchSetSelectedFile(updatedFiles); + }; + + const openFileSelector = (handleInputChange) => { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.onchange = (event) => { + const files = Array.from(event.target.files); + handleInputChange(event); }; + input.click(); + }; + + const handleCreateAssistant = async () => { + try { + console.log("Creating assistant with name:", selectedAssistant.name); + await postAssistant(auth.authToken, { name: selectedAssistant.name }); + const response = await getAssistants(auth.authToken); + setAssistants(response); + setAssistantCreated(true); + } catch (error) { + console.error('Error creating assistant:', error); + setShowAlert('Failed to create assistant.'); + } + }; + + const models = [ + { value: 'gpt-4-turbo', label: 'gpt-4-turbo' }, + { value: 'gpt-4', label: 'gpt-4' }, + { value: 'gpt-3.5-turbo-16k', label: 'gpt-3.5-turbo-16k' }, + { value: 'gpt-3.5-turbo-0125', label: 'gpt-3.5-turbo-0125' }, + { value: 'gpt-3.5-turbo', label: 'gpt-3.5-turbo' }, + { value: 'gpt-3.5-turbo', label: 'gpt-3.5-turbo' }, + ]; + + const largeDialogStyles = { + minWidth: '500px', + textAlign: 'left', + }; return ( - - - {showCreatePanel && ( - - - - {selectedAssistant.id ? 'Edit Assistant' : 'Create Assistant'} - - - setSelectedAssistant({ ...selectedAssistant, name: e.target.value })} - margin="normal" - /> - - setSelectedAssistant({ ...selectedAssistant, model: e.target.value })} - margin="normal" - sx={{ display: 'block', mt: 3, mb: 3 }} - SelectProps={{ - native: true, - }} - helperText="Please select your model" - > - {models.map((option) => ( - - ))} - - - Tools - - } - label="File search" - sx={{ display: 'block', mt: 2, mb: 2 }} - /> - - } - label="Code interpreter" - sx={{ display: 'block', mt: 2, mb: 2 }} - /> - - Functions - - MODEL CONFIGURATION - - Response format - - } - label="JSON object" - sx={{ display: 'block', mt: 1, mb: 1 }} - /> - Temperature - - Top P - - - - - )} - {/* Vista principal */} - - - Assistants - - - {loading ? ( - Loading... - ) : ( - assistants.length === 0 ? ( - No assistants available. - ) : ( - - - - - Date - Name - Actions - - - - {assistants.map((assistant) => ( - - {assistant.createdAt} - {assistant.name} - - - - - + + + + + {/* Container of Assistants */} + +
+ Assistants + {loading ? ( + Loading... + ) : ( + assistants.length === 0 && !assistantCreated ? ( + No assistants available. + ) : ( + +
+ + + Date + Name + Actions + + + + {assistants.map((assistant) => ( + + {assistant.createdAt} + {assistant.name} + + + + + + ))} + +
+
+ ) + )} + + + + + + {/* Container of Create Assistant */} + +
+ + {/* Creation panel */} + {showCreatePanel && ( + + + + {selectedAssistant.id ? 'Edit Assistant' : 'Create Assistant'} + + + setSelectedAssistant({ ...selectedAssistant, name: e.target.value })} + margin="normal" + /> + + setSelectedAssistant({ ...selectedAssistant, model: e.target.value })} + margin="normal" + sx={{ display: 'block', mt: 3, mb: 3 }} + SelectProps={{ + native: true, + }} + helperText="Please select your model" + > + {models.map((option) => ( + ))} - - - - ) - )} - {/* Diálogos de edición y eliminación */} - {/* Snackbar de alerta */} - + + Tools + +
+ } + label="File search" + sx={{ flex: '1', mt: 2, mb: 2 }} + /> + +
+ {fileSearchDialogOpen && ( + { + e.preventDefault(); + }} + onDragEnter={(e) => { + e.preventDefault(); + }} + onDrop={(e) => { + e.preventDefault(); + const files = Array.from(e.dataTransfer.files); + handleFileSearchInputChange({ target: { files } }); + }} + > + Attach files to file search + + + {fileSearchSelectedFile.length === 0 ? ( + <> + Drag your files here or{" "} + { + e.stopPropagation(); + openFileSelector(handleFileSearchInputChange); + }} + onMouseEnter={(e) => { + e.target.style.color = "darkblue"; + }} + onMouseLeave={(e) => { + e.target.style.color = "blue"; + }} + > + click to upload + + + Information in attached files will be available to this assistant. + + + ) : ( + <> + )} + + + {fileSearchSelectedFile.length > 0 && ( + + + + + File + Size + Uploaded + + + + {fileSearchSelectedFile.map((file, index) => ( + + {file.name} + {file.size} + {file.uploaded} + + + + + ))} + +
+
+ )} +
+ + + + {fileSearchSelectedFile.length > 0 && ( + + )} + + + +
+ + )} + {attachedFilesFileSearch.map((file, index) => ( + + File {index + 1}: {file.name} + + ))} + +
+ } + label="Code interpreter" + sx={{ display: 'block', mt: 2, mb: 2 }} + /> + +
+ + {codeInterpreterDialogOpen && ( + { + e.preventDefault(); + }} + onDragEnter={(e) => { + e.preventDefault(); + }} + onDrop={(e) => { + e.preventDefault(); + const files = Array.from(e.dataTransfer.files); + handleCodeInterpreterInputChange({ target: { files } }); + }} + > + Attach files to file search + + + {codeInterpreterSelectedFile.length === 0 ? ( + <> + Drag your files here or{" "} + { + e.stopPropagation(); + openFileSelector(handleCodeInterpreterInputChange); + }} + onMouseEnter={(e) => { + e.target.style.color = "darkblue"; + }} + onMouseLeave={(e) => { + e.target.style.color = "blue"; + }} + > + click to upload + + + Information in attached files will be available to this assistant. + + + ) : ( + <> + )} + + + {codeInterpreterSelectedFile.length > 0 && ( + + + + + File + Size + Uploaded + + + + {codeInterpreterSelectedFile.map((file, index) => ( + + {file.name} + {file.size} + {file.uploaded} + + + + + ))} + +
+
+ )} +
+ + + + {codeInterpreterSelectedFile.length > 0 && ( + + )} + + + +
+ )} + {attachedFilesCodeInterpreter.map((file, index) => ( + + File {index + 1}: {file.name} + + ))} + +
+ Functions + +
+ + +
+ MODEL CONFIGURATION + + Response format + + } + label="JSON object" + sx={{ display: 'block', mt: 1, mb: 3 }} + /> +
+ +
+
+ Temperature + +
+ +
+ Top P + +
+ +
+ +
+ +
+ +
+ )} +
+
- ); - } + + + +
+ ); + } \ No newline at end of file From 4f0e0b79d09ba52ff570a5ac8d10175c8250ae1e Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Mon, 13 May 2024 16:09:13 +0200 Subject: [PATCH 10/17] wip-createAssistant-endpoint-success --- examples/build.gradle.kts | 1 + server/build.gradle.kts | 2 ++ .../com/xebia/functional/xef/server/Server.kt | 2 +- .../xef/server/http/routes/AssistantRoutes.kt | 7 ++--- server/src/main/resources/logback.xml | 26 +++++++++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 server/src/main/resources/logback.xml diff --git a/examples/build.gradle.kts b/examples/build.gradle.kts index b29a285e2..6ea94bb67 100644 --- a/examples/build.gradle.kts +++ b/examples/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.jmf) implementation(libs.mp3.wav.converter) api(libs.ktor.client) + } spotless { diff --git a/server/build.gradle.kts b/server/build.gradle.kts index b9398e1e7..bf972c119 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -56,6 +56,8 @@ dependencies { testImplementation(libs.kotest.testcontainers) testImplementation(libs.testcontainers.postgresql) testRuntimeOnly(libs.kotest.junit5) + implementation(libs.logback) + implementation(libs.klogging) } spotless { diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt index 2566c167f..92aecf7ee 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt @@ -59,7 +59,7 @@ object Server { requestTimeout = 0 // disabled } install(Auth) - install(Logging) { level = LogLevel.INFO } + install(Logging) { level = LogLevel.ALL } install(ClientContentNegotiation) } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 02b64279d..179c1e99c 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -5,6 +5,7 @@ import com.xebia.functional.openai.generated.model.CreateAssistantRequest import com.xebia.functional.openai.generated.model.ListAssistantsResponse import com.xebia.functional.xef.Config import com.xebia.functional.xef.OpenAI +import com.xebia.functional.xef.llm.assistants.Assistant import com.xebia.functional.xef.server.models.Token import io.ktor.http.* import io.ktor.server.application.* @@ -63,10 +64,10 @@ fun Routing.assistantRoutes() { suspend fun createAssistant(token: Token, request: CreateAssistantRequest): AssistantObject { val openAIConfig = Config(token = token.value) - val openAI = OpenAI(openAIConfig) + val openAI = OpenAI(openAIConfig, logRequests = true) val assistants = openAI.assistants - val assistantObject = assistants.createAssistant(request) - return assistantObject + val assistant = Assistant(request) + return assistant.get() } // suspend fun updateAssistant(token: String, request: AssistantRequest, id: Int): String { diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml new file mode 100644 index 000000000..9a90533b5 --- /dev/null +++ b/server/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n + + + + + + + + + + + + + + + From d851f7e6389a79d4645d9fcb112717caed889358 Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Wed, 15 May 2024 16:21:25 +0200 Subject: [PATCH 11/17] assistant-endpoints-wip --- .../com/xebia/functional/xef/server/Server.kt | 3 +- .../xef/server/http/routes/AssistantRoutes.kt | 82 +++++++++++-------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt index 92aecf7ee..299468eae 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt @@ -63,7 +63,8 @@ object Server { install(ClientContentNegotiation) } - server(factory = Netty, port = 8081, host = "0.0.0.0") { + server(factory = Netty, port = 8081, host = "0.0.0.0" + ) { install(CORS) { allowNonSimpleContentTypes = true HttpMethod.DefaultMethods.forEach { allowMethod(it) } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 179c1e99c..0f9db33ad 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -2,11 +2,11 @@ package com.xebia.functional.xef.server.http.routes import com.xebia.functional.openai.generated.model.AssistantObject import com.xebia.functional.openai.generated.model.CreateAssistantRequest -import com.xebia.functional.openai.generated.model.ListAssistantsResponse +import com.xebia.functional.openai.generated.model.ModifyAssistantRequest import com.xebia.functional.xef.Config import com.xebia.functional.xef.OpenAI import com.xebia.functional.xef.llm.assistants.Assistant -import com.xebia.functional.xef.server.models.Token +import io.ktor.client.request.* import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* @@ -21,8 +21,7 @@ fun Routing.assistantRoutes() { val contentType = call.request.contentType() if (contentType == ContentType.Application.Json) { val request = call.receive() - val token = call.getToken() - val response = createAssistant(token, request) + val response = createAssistant(request) call.respond(status = HttpStatusCode.Created, response) } else { call.respond( @@ -36,19 +35,45 @@ fun Routing.assistantRoutes() { } } - // put("/v1/settings/assistants/{id}") { - // val request = Json.decodeFromString(call.receive()) - // val token = call.getToken() - // val id = call.getId() - // val response = updateAssistant(token, request, id) - // call.respond(status = HttpStatusCode.NoContent, response) - // } - // get("/v1/settings/assistants") { - // val token = call.getToken() - // val response = ListAssistantsResponse("list", emptyList(), null, null, false) - // val assistantResponse = listAssistants(token, response) - // call.respond(assistantResponse) - // } + get("/v1/settings/assistants") { + try { + val token = call.getToken() + val openAI = OpenAI(Config(token = token.value), logRequests = true) + val assistantsApi = openAI.assistants + val response = assistantsApi.listAssistants(configure = { + header("OpenAI-Beta", "assistants=v1") + }) + call.respond(HttpStatusCode.OK, response) + } catch (e: Exception) { + val trace = e.stackTraceToString() + call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") + } + } + + put("/v1/settings/assistants/{id}") { + try { + val contentType = call.request.contentType() + if (contentType == ContentType.Application.Json) { + val request = call.receive() + val id = call.parameters["id"] + if (id == null) { + call.respond(HttpStatusCode.BadRequest, "Invalid assistant id") + return@put + } + val response = updateAssistant(request, id) + call.respond(HttpStatusCode.OK, response) + } else { + call.respond( + HttpStatusCode.UnsupportedMediaType, + "Unsupported content type: $contentType" + ) + } + } catch (e: Exception) { + val trace = e.stackTraceToString() + call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") + } + } + // delete("/v1/settings/assistants/{id}") { // val token = call.getToken() // val id = call.parameters["id"]?.toIntOrNull() @@ -62,27 +87,16 @@ fun Routing.assistantRoutes() { } } -suspend fun createAssistant(token: Token, request: CreateAssistantRequest): AssistantObject { - val openAIConfig = Config(token = token.value) - val openAI = OpenAI(openAIConfig, logRequests = true) - val assistants = openAI.assistants +suspend fun createAssistant(request: CreateAssistantRequest): AssistantObject { val assistant = Assistant(request) return assistant.get() } -// suspend fun updateAssistant(token: String, request: AssistantRequest, id: Int): String { -// // Implement the logic for updating an assistant in OpenAI here -// } - -suspend fun listAssistants(token: Token, response: ListAssistantsResponse): ListAssistantsResponse { - val openAIConfig = Config(token = token.value) - val openAI = OpenAI(openAIConfig) - val assistants = openAI.assistants - val listAssistants = assistants.listAssistants() - - return listAssistants +suspend fun updateAssistant(request: ModifyAssistantRequest, id: String): AssistantObject { + val assistant = Assistant(id) + return assistant.modify(request).get() } /*suspend fun deleteAssistant(token: String, id: Int): String { - // Implement the logic for deleting an assistant in OpenAI here -}*/ + +}*/ \ No newline at end of file From 703d98b1de4799a49ab29361989b12d1416f1cd5 Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Wed, 15 May 2024 16:44:39 +0200 Subject: [PATCH 12/17] assistant endpoints success --- .../xef/server/http/routes/AssistantRoutes.kt | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 0f9db33ad..551916857 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -74,16 +74,25 @@ fun Routing.assistantRoutes() { } } - // delete("/v1/settings/assistants/{id}") { - // val token = call.getToken() - // val id = call.parameters["id"]?.toIntOrNull() - // if (id == null) { - // call.respond(HttpStatusCode.BadRequest, "Invalid assistant id") - // return@delete - // } - // val response = deleteAssistant(token, id) - // call.respond(status = HttpStatusCode.NoContent, response) - // } + delete("/v1/settings/assistants/{id}") { + try { + val token = call.getToken() + val id = call.parameters["id"] + if (id == null) { + call.respond(HttpStatusCode.BadRequest, "Invalid assistant id") + return@delete + } + val openAI = OpenAI(Config(token = token.value), logRequests = true) + val assistantsApi = openAI.assistants + val response = assistantsApi.deleteAssistant(id, configure = { + header("OpenAI-Beta", "assistants=v1") + }) + call.respond(status = HttpStatusCode.NoContent, response) + } catch (e: Exception) { + val trace = e.stackTraceToString() + call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") + } + } } } @@ -95,8 +104,4 @@ suspend fun createAssistant(request: CreateAssistantRequest): AssistantObject { suspend fun updateAssistant(request: ModifyAssistantRequest, id: String): AssistantObject { val assistant = Assistant(id) return assistant.modify(request).get() -} - -/*suspend fun deleteAssistant(token: String, id: Int): String { - -}*/ \ No newline at end of file +} \ No newline at end of file From e447dae3c25d0fb23cf404f6592aea5096e771a5 Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Wed, 15 May 2024 15:00:00 +0000 Subject: [PATCH 13/17] Apply spotless formatting --- .../kotlin/com/xebia/functional/xef/server/Server.kt | 3 +-- .../xef/server/http/routes/AssistantRoutes.kt | 12 +++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt index 299468eae..92aecf7ee 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt @@ -63,8 +63,7 @@ object Server { install(ClientContentNegotiation) } - server(factory = Netty, port = 8081, host = "0.0.0.0" - ) { + server(factory = Netty, port = 8081, host = "0.0.0.0") { install(CORS) { allowNonSimpleContentTypes = true HttpMethod.DefaultMethods.forEach { allowMethod(it) } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 551916857..4ad2bcdc8 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -40,9 +40,8 @@ fun Routing.assistantRoutes() { val token = call.getToken() val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants - val response = assistantsApi.listAssistants(configure = { - header("OpenAI-Beta", "assistants=v1") - }) + val response = + assistantsApi.listAssistants(configure = { header("OpenAI-Beta", "assistants=v1") }) call.respond(HttpStatusCode.OK, response) } catch (e: Exception) { val trace = e.stackTraceToString() @@ -84,9 +83,8 @@ fun Routing.assistantRoutes() { } val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants - val response = assistantsApi.deleteAssistant(id, configure = { - header("OpenAI-Beta", "assistants=v1") - }) + val response = + assistantsApi.deleteAssistant(id, configure = { header("OpenAI-Beta", "assistants=v1") }) call.respond(status = HttpStatusCode.NoContent, response) } catch (e: Exception) { val trace = e.stackTraceToString() @@ -104,4 +102,4 @@ suspend fun createAssistant(request: CreateAssistantRequest): AssistantObject { suspend fun updateAssistant(request: ModifyAssistantRequest, id: String): AssistantObject { val assistant = Assistant(id) return assistant.modify(request).get() -} \ No newline at end of file +} From 71e882e59a13ce4dc45d55e43ce7c743a5de606f Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Tue, 21 May 2024 16:48:00 +0200 Subject: [PATCH 14/17] some corrections OK, added icons OK, and assistants.tsx WIP --- .../com/xebia/functional/xef/server/Server.kt | 2 +- .../xef/server/http/routes/AssistantRoutes.kt | 27 ++- .../xef/server/http/routes/XefRoutes.kt | 2 +- server/src/main/resources/logback.xml | 2 +- server/web/src/assets/assistants-icon.svg | 1 + server/web/src/assets/chat-icon.svg | 1 + .../web/src/assets/generic-question-icon.svg | 1 + server/web/src/assets/home-icon.svg | 1 + server/web/src/assets/organizations-icon.svg | 1 + server/web/src/assets/projects-icon.svg | 1 + server/web/src/assets/settings-icon.svg | 1 + .../Pages/Assistants/Assistants.module.css | 14 ++ .../Pages/Assistants/Assistants.tsx | 57 ++++-- .../src/components/Sidebar/Sidebar.module.css | 4 + server/web/src/components/Sidebar/Sidebar.tsx | 17 +- server/web/src/utils/api/assistants.ts | 174 ++++++++---------- server/web/src/utils/api/chatCompletions.ts | 5 - 17 files changed, 171 insertions(+), 140 deletions(-) create mode 100644 server/web/src/assets/assistants-icon.svg create mode 100644 server/web/src/assets/chat-icon.svg create mode 100644 server/web/src/assets/generic-question-icon.svg create mode 100644 server/web/src/assets/home-icon.svg create mode 100644 server/web/src/assets/organizations-icon.svg create mode 100644 server/web/src/assets/projects-icon.svg create mode 100644 server/web/src/assets/settings-icon.svg create mode 100644 server/web/src/components/Pages/Assistants/Assistants.module.css diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt index 299468eae..915f1ac54 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/Server.kt @@ -59,7 +59,7 @@ object Server { requestTimeout = 0 // disabled } install(Auth) - install(Logging) { level = LogLevel.ALL } + install(Logging) { level = LogLevel.INFO } install(ClientContentNegotiation) } diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 551916857..21f7cf8b7 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -1,11 +1,11 @@ package com.xebia.functional.xef.server.http.routes -import com.xebia.functional.openai.generated.model.AssistantObject import com.xebia.functional.openai.generated.model.CreateAssistantRequest import com.xebia.functional.openai.generated.model.ModifyAssistantRequest import com.xebia.functional.xef.Config import com.xebia.functional.xef.OpenAI import com.xebia.functional.xef.llm.assistants.Assistant +import io.github.oshai.kotlinlogging.KLogger import io.ktor.client.request.* import io.ktor.http.* import io.ktor.server.application.* @@ -14,14 +14,16 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Routing.assistantRoutes() { +fun Routing.assistantRoutes(logger: KLogger) { authenticate("auth-bearer") { post("/v1/settings/assistants") { try { val contentType = call.request.contentType() if (contentType == ContentType.Application.Json) { val request = call.receive() - val response = createAssistant(request) + val assistant = Assistant(request) + val response = assistant.get() + logger.info("Created assistant: ${response.name} with id: ${response.id}") call.respond(status = HttpStatusCode.Created, response) } else { call.respond( @@ -46,6 +48,7 @@ fun Routing.assistantRoutes() { call.respond(HttpStatusCode.OK, response) } catch (e: Exception) { val trace = e.stackTraceToString() + logger.error("Error creating assistant: $trace") call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") } } @@ -60,7 +63,9 @@ fun Routing.assistantRoutes() { call.respond(HttpStatusCode.BadRequest, "Invalid assistant id") return@put } - val response = updateAssistant(request, id) + val assistant = Assistant(id) + val response = assistant.modify(request).get() + logger.info("Modified assistant: ${response.name} with id: ${response.id}") call.respond(HttpStatusCode.OK, response) } else { call.respond( @@ -70,6 +75,7 @@ fun Routing.assistantRoutes() { } } catch (e: Exception) { val trace = e.stackTraceToString() + logger.error("Error modifying assistant: $trace") call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") } } @@ -84,24 +90,17 @@ fun Routing.assistantRoutes() { } val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants + val assistant = assistantsApi.getAssistant(id) val response = assistantsApi.deleteAssistant(id, configure = { header("OpenAI-Beta", "assistants=v1") }) + logger.info("Deleted assistant: ${assistant.name} with id: ${response.id}") call.respond(status = HttpStatusCode.NoContent, response) } catch (e: Exception) { val trace = e.stackTraceToString() + logger.error("Error deleting assistant: $trace") call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") } } } -} - -suspend fun createAssistant(request: CreateAssistantRequest): AssistantObject { - val assistant = Assistant(request) - return assistant.get() -} - -suspend fun updateAssistant(request: ModifyAssistantRequest, id: String): AssistantObject { - val assistant = Assistant(id) - return assistant.modify(request).get() } \ No newline at end of file diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt index a4dcbfbb6..7780c3ce9 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/XefRoutes.kt @@ -12,5 +12,5 @@ fun Routing.xefRoutes(logger: KLogger) { organizationRoutes(OrganizationRepositoryService(logger)) projectsRoutes(ProjectRepositoryService(logger)) tokensRoutes(TokenRepositoryService(logger)) - assistantRoutes() + assistantRoutes(logger) } diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index 9a90533b5..fb058d664 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -12,7 +12,7 @@ - + diff --git a/server/web/src/assets/assistants-icon.svg b/server/web/src/assets/assistants-icon.svg new file mode 100644 index 000000000..fcc7189f0 --- /dev/null +++ b/server/web/src/assets/assistants-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/assets/chat-icon.svg b/server/web/src/assets/chat-icon.svg new file mode 100644 index 000000000..bd5fca737 --- /dev/null +++ b/server/web/src/assets/chat-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/assets/generic-question-icon.svg b/server/web/src/assets/generic-question-icon.svg new file mode 100644 index 000000000..a3de74801 --- /dev/null +++ b/server/web/src/assets/generic-question-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/assets/home-icon.svg b/server/web/src/assets/home-icon.svg new file mode 100644 index 000000000..fbebaf2d2 --- /dev/null +++ b/server/web/src/assets/home-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/assets/organizations-icon.svg b/server/web/src/assets/organizations-icon.svg new file mode 100644 index 000000000..7b4e35ef4 --- /dev/null +++ b/server/web/src/assets/organizations-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/assets/projects-icon.svg b/server/web/src/assets/projects-icon.svg new file mode 100644 index 000000000..812813e2b --- /dev/null +++ b/server/web/src/assets/projects-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/assets/settings-icon.svg b/server/web/src/assets/settings-icon.svg new file mode 100644 index 000000000..8cb503db9 --- /dev/null +++ b/server/web/src/assets/settings-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/server/web/src/components/Pages/Assistants/Assistants.module.css b/server/web/src/components/Pages/Assistants/Assistants.module.css new file mode 100644 index 000000000..8d3575269 --- /dev/null +++ b/server/web/src/components/Pages/Assistants/Assistants.module.css @@ -0,0 +1,14 @@ +.container { + position: relative; + height: 100%; +} + +.sliders { + display: flex; + justify-content: space-between; + align-items: center; +} + +.boldText { + font-weight: bold; +} \ No newline at end of file diff --git a/server/web/src/components/Pages/Assistants/Assistants.tsx b/server/web/src/components/Pages/Assistants/Assistants.tsx index 83e2a61b9..4a0fef6bf 100644 --- a/server/web/src/components/Pages/Assistants/Assistants.tsx +++ b/server/web/src/components/Pages/Assistants/Assistants.tsx @@ -1,6 +1,7 @@ import { useContext, useEffect, useState, ChangeEvent } from "react"; import { useAuth } from "@/state/Auth"; import { LoadingContext } from "@/state/Loading"; +import styles from './Assistants.module.css'; import { Alert, @@ -157,25 +158,26 @@ export function Assistants() { }; const handleCreateAssistant = async () => { - try { - console.log("Creating assistant with name:", selectedAssistant.name); - await postAssistant(auth.authToken, { name: selectedAssistant.name }); - const response = await getAssistants(auth.authToken); - setAssistants(response); - setAssistantCreated(true); - } catch (error) { - console.error('Error creating assistant:', error); - setShowAlert('Failed to create assistant.'); - } + }; const models = [ + { value: 'gpt-4o-2024-05-13', label: 'gpt-4o-2024-05-13' }, + { value: 'gpt-4o', label: 'gpt-4o' }, + { value: 'gpt-4-vision-preview', label: 'gpt-4-vision-preview' }, + { value: 'gpt-4-turbo-preview', label: 'gpt-4-turbo-preview' }, + { value: 'gpt-4-2024-04-09', label: 'gpt-4-2024-04-09' }, { value: 'gpt-4-turbo', label: 'gpt-4-turbo' }, + { value: 'gpt-4-1106-preview', label: 'gpt-4-1106-preview' }, + { value: 'gpt-4-0613', label: 'gpt-4-0613' }, + { value: 'gpt-4-0125-preview', label: 'gpt-4-0125-preview' }, { value: 'gpt-4', label: 'gpt-4' }, + { value: 'gpt-3.5-turbo-16k-0613', label: 'gpt-3.5-turbo-16k-0613' }, { value: 'gpt-3.5-turbo-16k', label: 'gpt-3.5-turbo-16k' }, + { value: 'gpt-3.5-turbo-1106', label: 'gpt-3.5-turbo-1106' }, + { value: 'gpt-3.5-turbo-0613', label: 'gpt-3.5-turbo-0613' }, { value: 'gpt-3.5-turbo-0125', label: 'gpt-3.5-turbo-0125' }, { value: 'gpt-3.5-turbo', label: 'gpt-3.5-turbo' }, - { value: 'gpt-3.5-turbo', label: 'gpt-3.5-turbo' }, ]; const largeDialogStyles = { @@ -183,8 +185,23 @@ export function Assistants() { textAlign: 'left', }; + useEffect(() => { + const fetchAssistants = async () => { + try { + const response = await axios.get('/v1/settings/assistants'); // Endpoint para obtener la lista de asistentes + setAssistants(response.data); // Actualizar el estado con los datos de los asistentes + setLoading(false); // Indicar que la carga ha finalizado + } catch (error) { + console.error('Error fetching assistants:', error); + setLoading(false); // Indicar que la carga ha finalizado incluso en caso de error + } + }; + + fetchAssistants(); // Llamar a la función para obtener los asistentes cuando el componente se monta + }, []); // El segundo argumento [] asegura que esta función solo se ejecute una vez, cuando el componente se monta + return ( - + @@ -210,7 +227,7 @@ export function Assistants() { {assistants.map((assistant) => ( - {assistant.createdAt} + {assistant.id} {assistant.name} + {fileSearchDialogOpen && ( - + {fileSearchSelectedFile.length > 0 && ( @@ -512,9 +529,9 @@ export function Assistants() {
- MODEL CONFIGURATION + MODEL CONFIGURATION - Response format + Response format } @@ -524,7 +541,7 @@ export function Assistants() {
-
+
Temperature -
+
Top P - + Home + + Organizations - {/* Puedes agregar un ícono si es necesario, por ejemplo */} + Assistants + Projects + Chat + Generic question + Settings diff --git a/server/web/src/utils/api/assistants.ts b/server/web/src/utils/api/assistants.ts index f7f5556fd..92425ba42 100644 --- a/server/web/src/utils/api/assistants.ts +++ b/server/web/src/utils/api/assistants.ts @@ -1,110 +1,92 @@ -// Este código asume que tienes un enum similar para los endpoints de la API de asistentes import { - ApiOptions, - EndpointsEnum, - apiConfigConstructor, - apiFetch, - baseHeaders, - defaultApiServer, -} from '@/utils/api'; + ApiOptions, + EndpointsEnum, + apiConfigConstructor, + apiFetch, + baseHeaders, + defaultApiServer, + } from '@/utils/api'; -// Definiciones de tipos para las respuestas y solicitudes de asistentes (ajústalas a tu API) -export type AssistantRequest = { - name: string; - // Otros campos relevantes para la creación o actualización de un asistente -}; + export type CreateAssistantRequest = { + model: string; + name?: string; + description?: string; + instructions?: string; + tools?: AssistantTool[]; + fileIds?: string[]; + metadata?: Record; + }; -export type AssistantResponse = { - id: number; - name: string; - createdAt: string; - // Otros campos que tu API devuelve -}; + export type ModifyAssistantRequest = { + model?: string; + name?: string; + description?: string; + instructions?: string; + tools?: AssistantTool[]; + fileIds?: string[]; + metadata?: Record; + }; -const assistantApiBaseOptions: ApiOptions = { - endpointServer: defaultApiServer, - endpointPath: EndpointsEnum.assistant, // Asegúrate de que este enum existe y es correcto - endpointValue: '', - requestOptions: { - headers: baseHeaders, - }, -}; + type CodeInterpreter = { + type: 'code_interpreter'; + }; -// POST: Crear un nuevo asistente -export async function postAssistant(authToken: string, data: AssistantRequest): Promise { - const apiOptions: ApiOptions = { - ...assistantApiBaseOptions, - body: JSON.stringify(data), - requestOptions: { - method: 'POST', - ...assistantApiBaseOptions.requestOptions, - headers: { - ...assistantApiBaseOptions.requestOptions.headers, - Authorization: `Bearer ${authToken}`, - }, - }, + type Retrieval = { + type: 'retrieval'; }; - const apiConfig = apiConfigConstructor(apiOptions); - const response = await apiFetch(apiConfig); - return response.status; // Asumiendo que la API devuelve un código de estado para representar el resultado -} -// GET: Obtener todos los asistentes -export async function getAssistants(authToken: string): Promise { - const apiOptions: ApiOptions = { - ...assistantApiBaseOptions, - requestOptions: { - method: 'GET', - ...assistantApiBaseOptions.requestOptions, - headers: { - ...assistantApiBaseOptions.requestOptions.headers, - Authorization: `Bearer ${authToken}`, - }, - }, + type Function = { + type: 'function'; + name: string; + description: string; + parameters: string; }; - const apiConfig = apiConfigConstructor(apiOptions); - const response = await apiFetch(apiConfig); - if (!response.data) { - throw new Error('No assistants data returned from the API'); - } - return response.data; -} -// PUT: Actualizar un asistente existente -export async function putAssistant(authToken: string, id: number, data: AssistantRequest): Promise { - const apiOptions: ApiOptions = { - ...assistantApiBaseOptions, - endpointValue: `/${id}`, - body: JSON.stringify(data), + type AssistantTool = CodeInterpreter | Retrieval | Function; + + const assistantApiBaseOptions: ApiOptions = { + endpointServer: defaultApiServer, + endpointPath: EndpointsEnum.assistant, + endpointValue: '', requestOptions: { - method: 'PUT', - ...assistantApiBaseOptions.requestOptions, - headers: { - ...assistantApiBaseOptions.requestOptions.headers, - Authorization: `Bearer ${authToken}`, - }, + headers: baseHeaders, }, }; - const apiConfig = apiConfigConstructor(apiOptions); - const response = await apiFetch(apiConfig); - return response.status; -} -// DELETE: Eliminar un asistente -export async function deleteAssistant(authToken: string, id: number): Promise { - const apiOptions: ApiOptions = { - ...assistantApiBaseOptions, - endpointValue: `/${id}`, - requestOptions: { - method: 'DELETE', - ...assistantApiBaseOptions.requestOptions, - headers: { - ...assistantApiBaseOptions.requestOptions.headers, - Authorization: `Bearer ${authToken}`, + async function executeRequest(authToken: string, method: string, endpointValue: string = '', body?: any): Promise { + const apiOptions: ApiOptions = { + ...assistantApiBaseOptions, + endpointValue: endpointValue, + body: body ? JSON.stringify(body) : undefined, + requestOptions: { + method: method, + ...assistantApiBaseOptions.requestOptions, + headers: { + ...assistantApiBaseOptions.requestOptions.headers, + Authorization: `Bearer ${authToken}`, + }, }, - }, - }; - const apiConfig = apiConfigConstructor(apiOptions); - const response = await apiFetch(apiConfig); - return response.status; -} + }; + const apiConfig = apiConfigConstructor(apiOptions); + const response = await apiFetch(apiConfig); + if (!response.data) { + throw new Error('No data returned from the API'); + } + return response.data; + } + + export async function postAssistant(authToken: string, data: CreateAssistantRequest): Promise { + return executeRequest(authToken, 'POST', '', data); + } + + export async function getAssistants(authToken: string): Promise { + return executeRequest(authToken, 'GET'); + } + + export async function putAssistant(authToken: string, id: string, data: ModifyAssistantRequest): Promise { + return executeRequest(authToken, 'PUT', `/${id}`, data); + } + + export async function deleteAssistant(authToken: string, id: string): Promise { + await executeRequest(authToken, 'DELETE', `/${id}`); + } \ No newline at end of file diff --git a/server/web/src/utils/api/chatCompletions.ts b/server/web/src/utils/api/chatCompletions.ts index 52e06a33b..adf4fb61c 100644 --- a/server/web/src/utils/api/chatCompletions.ts +++ b/server/web/src/utils/api/chatCompletions.ts @@ -1,14 +1,9 @@ -/*import { - defaultApiServer, -} from '@/utils/api';*/ - import {OpenAI} from "openai/index"; import {Settings} from "@/state/Settings"; export function openai (settings: Settings): OpenAI { if (!settings.apiKey) throw 'API key not set'; return new OpenAI({ - //baseURL: defaultApiServer, // TODO: remove this when the key is the user token used for client auth dangerouslyAllowBrowser: true, apiKey: settings.apiKey, // defaults to process.env["OPENAI_API_KEY"] From 2232702965d249e3e4cd411c91324dae25a03285 Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Wed, 22 May 2024 12:34:56 +0200 Subject: [PATCH 15/17] some corrections in assistants.ts WIP and assistants.tsx WIP --- .../Pages/Assistants/Assistants.tsx | 30 ++++++++++--------- server/web/src/utils/api/assistants.ts | 6 ++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/server/web/src/components/Pages/Assistants/Assistants.tsx b/server/web/src/components/Pages/Assistants/Assistants.tsx index 4a0fef6bf..e754b52b6 100644 --- a/server/web/src/components/Pages/Assistants/Assistants.tsx +++ b/server/web/src/components/Pages/Assistants/Assistants.tsx @@ -185,20 +185,22 @@ export function Assistants() { textAlign: 'left', }; - useEffect(() => { - const fetchAssistants = async () => { - try { - const response = await axios.get('/v1/settings/assistants'); // Endpoint para obtener la lista de asistentes - setAssistants(response.data); // Actualizar el estado con los datos de los asistentes - setLoading(false); // Indicar que la carga ha finalizado - } catch (error) { - console.error('Error fetching assistants:', error); - setLoading(false); // Indicar que la carga ha finalizado incluso en caso de error - } - }; - - fetchAssistants(); // Llamar a la función para obtener los asistentes cuando el componente se monta - }, []); // El segundo argumento [] asegura que esta función solo se ejecute una vez, cuando el componente se monta + useEffect(() => { + const fetchAssistants = async () => { + setLoading(true); + try { + const fetchedAssistants = await getAssistants(auth.token); + setAssistants(fetchedAssistants); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + fetchAssistants(); + }, [auth.token]); + return ( diff --git a/server/web/src/utils/api/assistants.ts b/server/web/src/utils/api/assistants.ts index 92425ba42..b3b0af7b1 100644 --- a/server/web/src/utils/api/assistants.ts +++ b/server/web/src/utils/api/assistants.ts @@ -76,15 +76,15 @@ import { } export async function postAssistant(authToken: string, data: CreateAssistantRequest): Promise { - return executeRequest(authToken, 'POST', '', data); + return executeRequest(authToken, 'POST', '', data); } export async function getAssistants(authToken: string): Promise { - return executeRequest(authToken, 'GET'); + return executeRequest(authToken, 'GET'); } export async function putAssistant(authToken: string, id: string, data: ModifyAssistantRequest): Promise { - return executeRequest(authToken, 'PUT', `/${id}`, data); + return executeRequest(authToken, 'PUT', `/${id}`, data); } export async function deleteAssistant(authToken: string, id: string): Promise { From 535ad4affa31d1e3a321a10aff0ef5518b858639 Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Wed, 22 May 2024 13:18:19 +0200 Subject: [PATCH 16/17] format --- .../xebia/functional/xef/server/http/routes/AssistantRoutes.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 21f7cf8b7..8dad34236 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -14,7 +14,7 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Routing.assistantRoutes(logger: KLogger) { +fun Routing.assistantRoutes(logger: KLogger) { authenticate("auth-bearer") { post("/v1/settings/assistants") { try { From d8522061e7e85c647942305651a71000d173f7b9 Mon Sep 17 00:00:00 2001 From: mccarrascog Date: Thu, 23 May 2024 08:03:22 +0000 Subject: [PATCH 17/17] Apply spotless formatting --- .../xef/server/http/routes/AssistantRoutes.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 21f7cf8b7..2277ed4f2 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -42,9 +42,8 @@ fun Routing.assistantRoutes(logger: KLogger) { val token = call.getToken() val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants - val response = assistantsApi.listAssistants(configure = { - header("OpenAI-Beta", "assistants=v1") - }) + val response = + assistantsApi.listAssistants(configure = { header("OpenAI-Beta", "assistants=v1") }) call.respond(HttpStatusCode.OK, response) } catch (e: Exception) { val trace = e.stackTraceToString() @@ -91,9 +90,8 @@ fun Routing.assistantRoutes(logger: KLogger) { val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants val assistant = assistantsApi.getAssistant(id) - val response = assistantsApi.deleteAssistant(id, configure = { - header("OpenAI-Beta", "assistants=v1") - }) + val response = + assistantsApi.deleteAssistant(id, configure = { header("OpenAI-Beta", "assistants=v1") }) logger.info("Deleted assistant: ${assistant.name} with id: ${response.id}") call.respond(status = HttpStatusCode.NoContent, response) } catch (e: Exception) { @@ -103,4 +101,4 @@ fun Routing.assistantRoutes(logger: KLogger) { } } } -} \ No newline at end of file +}