Skip to content

feat: Add custom function support for OpenAI-o3 model #1826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,282 changes: 1,179 additions & 103 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@
}
]
},
"devEngines": {
"engines": {
"node": ">=14.x",
"npm": ">=7.x"
},
Expand Down
4 changes: 4 additions & 0 deletions release/app/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


306 changes: 306 additions & 0 deletions src/renderer/components/AddFunction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
import React, { useState, useEffect } from 'react'
import {
Button,
TextField,
Typography,
Box,
List,
ListItem,
IconButton,
Switch,
FormControlLabel
} from '@mui/material'
import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit'
import SaveIcon from '@mui/icons-material/Save'
import CancelIcon from '@mui/icons-material/Cancel'
import { useTranslation } from 'react-i18next'

export interface AddFunctionType {
// "name" can be used to label or group these definitions.
name: string
// "description" contains the entire JSON schema that the user wants to define.
description: string
}

export interface Props {
functions: AddFunctionType[]
onFunctionsChange: (functions: AddFunctionType[]) => void
}

export default function AddFunction(props: Props) {
const { t } = useTranslation()

// Load the saved enable/disable state from localStorage
const [functionsEnabled, setFunctionsEnabled] = useState<boolean>(() => {
const saved = localStorage.getItem('functionsEnabled')
return saved ? JSON.parse(saved) : false
})

// States for creating a new function
const [newFunctionName, setNewFunctionName] = useState('')
const [newFunctionDescription, setNewFunctionDescription] = useState('')

// States for editing an existing function
const [editingIndex, setEditingIndex] = useState<number | null>(null)
const [editingName, setEditingName] = useState('')
const [editingDescription, setEditingDescription] = useState('')

// If functions are enabled on mount, save to localStorage
useEffect(() => {
if (functionsEnabled) {
localStorage.setItem('functions', JSON.stringify(props.functions))
}
}, [])

// Save the enable/disable flag whenever it changes
useEffect(() => {
localStorage.setItem('functionsEnabled', JSON.stringify(functionsEnabled))
}, [functionsEnabled])

// Save entire function objects whenever they change
useEffect(() => {
if (functionsEnabled) {
localStorage.setItem('functions', JSON.stringify(props.functions))
}
}, [props.functions, functionsEnabled])

// We can keep a basic naming rule, or remove if you prefer
const isValidFunctionName = (name: string): boolean => {
return /^[a-zA-Z0-9_]+$/.test(name)
}

const handleToggleFunctions = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setFunctionsEnabled(event.target.checked)
if (!event.target.checked) {
// Clear saved functions if user toggles off
localStorage.removeItem('functions')
} else {
// Restore or re-save if toggled on
localStorage.setItem('functions', JSON.stringify(props.functions))
}
}

const handleAddFunction = () => {
if (newFunctionName && newFunctionDescription && isValidFunctionName(newFunctionName)) {
try {
const parsedDescription = JSON.parse(newFunctionDescription);

// Ensure the parsed description has the correct structure
if (!parsedDescription.type || parsedDescription.type !== 'function' || !parsedDescription.function) {
throw new Error("Invalid function structure");
}

const newFunction = {
type: "function",
function: {
name: parsedDescription.function.name,
description: parsedDescription.function.description,
parameters: parsedDescription.function.parameters
}
};

const updatedFunctions = [...props.functions, newFunction];
props.onFunctionsChange(updatedFunctions);
localStorage.setItem('functions', JSON.stringify(updatedFunctions));
setNewFunctionName('');
setNewFunctionDescription('');
} catch (error) {
console.error("Error parsing function JSON:", error);
// Show an error message to the user
}
}
};




const handleRemoveFunction = (index: number) => {
const updatedFunctions = props.functions.filter((_, i) => i !== index)
props.onFunctionsChange(updatedFunctions)
localStorage.setItem('functions', JSON.stringify(updatedFunctions))
}

const handleEditFunction = (index: number) => {
const func = props.functions[index]
setEditingIndex(index)
setEditingName(func.name)
setEditingDescription(func.description)
}

const handleSaveEdit = (index: number) => {
if (isValidFunctionName(editingName)) {
const updatedFunctions = [...props.functions]
updatedFunctions[index] = {
name: editingName,
description: editingDescription
}
props.onFunctionsChange(updatedFunctions)
localStorage.setItem('functions', JSON.stringify(updatedFunctions))

setEditingIndex(null)
setEditingName('')
setEditingDescription('')
}
}

const handleCancelEdit = () => {
setEditingIndex(null)
setEditingName('')
setEditingDescription('')
}

return (
<Box sx={{ mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={functionsEnabled}
onChange={handleToggleFunctions}
color="primary"
/>
}
label={t('Enable Functions')}
/>
{functionsEnabled && (
<>
<Typography variant="subtitle1" gutterBottom>
{t('Functions')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<TextField
label={t('Function Name')}
value={newFunctionName}
onChange={(e) => setNewFunctionName(e.target.value)}
fullWidth
size="small"
error={
!isValidFunctionName(newFunctionName) &&
newFunctionName !== ''
}
helperText={
!isValidFunctionName(newFunctionName) &&
newFunctionName !== ''
? 'Invalid function name. Use only letters, numbers, and underscores.'
: ''
}
/>
<TextField
label={t('Function JSON Schema')}
value={newFunctionDescription}
onChange={(e) => setNewFunctionDescription(e.target.value)}
fullWidth
size="small"
multiline
minRows={4}
helperText="Paste your entire JSON function schema here."
/>
<Button
variant="contained"
onClick={handleAddFunction}
disabled={
!newFunctionName ||
!newFunctionDescription ||
!isValidFunctionName(newFunctionName)
}
>
{t('Add Function')}
</Button>
</Box>
<List>
{props.functions.map((func, index) => (
<ListItem
key={index}
secondaryAction={
editingIndex === index ? (
<>
<IconButton
edge="end"
aria-label="save"
onClick={() => handleSaveEdit(index)}
>
<SaveIcon />
</IconButton>
<IconButton
edge="end"
aria-label="cancel"
onClick={handleCancelEdit}
>
<CancelIcon />
</IconButton>
</>
) : (
<>
<IconButton
edge="end"
aria-label="edit"
onClick={() => handleEditFunction(index)}
>
<EditIcon />
</IconButton>
<IconButton
edge="end"
aria-label="delete"
onClick={() => handleRemoveFunction(index)}
>
<DeleteIcon />
</IconButton>
</>
)
}
>
{editingIndex === index ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 1,
width: '100%'
}}
>
<TextField
label={t('Function Name')}
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
fullWidth
size="small"
error={!isValidFunctionName(editingName)}
helperText={
!isValidFunctionName(editingName)
? 'Invalid function name. Use only letters, numbers, and underscores.'
: ''
}
/>
<TextField
label={t('Function JSON Schema')}
value={editingDescription}
onChange={(e) => setEditingDescription(e.target.value)}
fullWidth
size="small"
multiline
minRows={4}
/>
</Box>
) : (
<Box>
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
{func.name}
</Typography>
<Typography
variant="body2"
sx={{ whiteSpace: 'pre-wrap', mt: 1 }}
>
{func.description}
</Typography>
</Box>
)}
</ListItem>
))}
</List>
</>
)}
</Box>
)
}
Empty file.
45 changes: 45 additions & 0 deletions src/renderer/components/ReasoningEffortSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useEffect } from 'react'
import { Select, MenuItem, Typography, Box } from '@mui/material'
import { useTranslation } from 'react-i18next'

export interface Props {
value: string
onChange: (value: string) => void
className?: string
}

export default function ReasoningEffortSelect(props: Props) {
const { t } = useTranslation()

useEffect(() => {
if (!props.value) {
props.onChange('medium')
}
}, [])

const handleChange = (event: any) => {
props.onChange(event.target.value as string)
}

return (
<Box sx={{ margin: '10px' }} className={props.className}>
<Box>
<Typography id="reasoning-effort-select" gutterBottom>
{t('Reasoning Effort')}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Select
value={props.value || 'medium'}
onChange={handleChange}
fullWidth
size="small"
>
<MenuItem value="low">{t('Low')}</MenuItem>
<MenuItem value="medium">{t('Medium')}</MenuItem>
<MenuItem value="high">{t('High')}</MenuItem>
</Select>
</Box>
</Box>
)
}
Loading