forked from Shinonome/dots-hyprland
ai: gemini: files
This commit is contained in:
@@ -40,7 +40,6 @@ Singleton {
|
|||||||
* @returns { string }
|
* @returns { string }
|
||||||
*/
|
*/
|
||||||
function shellSingleQuoteEscape(str) {
|
function shellSingleQuoteEscape(str) {
|
||||||
// escape single quotes
|
|
||||||
return String(str)
|
return String(str)
|
||||||
// .replace(/\\/g, '\\\\')
|
// .replace(/\\/g, '\\\\')
|
||||||
.replace(/'/g, "'\\''");
|
.replace(/'/g, "'\\''");
|
||||||
|
|||||||
@@ -38,6 +38,13 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property var allCommands: [
|
property var allCommands: [
|
||||||
|
{
|
||||||
|
name: "attach",
|
||||||
|
description: Translation.tr("Attach a file. Only works with Gemini."),
|
||||||
|
execute: (args) => {
|
||||||
|
Ai.attachFile(args.join(" ").trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "model",
|
name: "model",
|
||||||
description: Translation.tr("Choose model"),
|
description: Translation.tr("Choose model"),
|
||||||
@@ -421,13 +428,13 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
|||||||
|
|
||||||
Rectangle { // Input area
|
Rectangle { // Input area
|
||||||
id: inputWrapper
|
id: inputWrapper
|
||||||
property real columnSpacing: 5
|
property real spacing: 5
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
radius: Appearance.rounding.small
|
radius: Appearance.rounding.small
|
||||||
color: Appearance.colors.colLayer1
|
color: Appearance.colors.colLayer1
|
||||||
implicitWidth: messageInputField.implicitWidth
|
|
||||||
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
|
implicitHeight: Math.max(inputFieldRowLayout.implicitHeight + inputFieldRowLayout.anchors.topMargin
|
||||||
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + columnSpacing, 45)
|
+ commandButtonsRow.implicitHeight + commandButtonsRow.anchors.bottomMargin + spacing, 45)
|
||||||
|
+ (attachedFileIndicator.implicitHeight + spacing + attachedFileIndicator.anchors.topMargin)
|
||||||
clip: true
|
clip: true
|
||||||
border.color: Appearance.colors.colOutlineVariant
|
border.color: Appearance.colors.colOutlineVariant
|
||||||
border.width: 1
|
border.width: 1
|
||||||
@@ -436,12 +443,26 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
|
|||||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AttachedFileIndicator {
|
||||||
|
id: attachedFileIndicator
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: visible ? 5 : 0
|
||||||
|
}
|
||||||
|
filePath: Ai.pendingFilePath
|
||||||
|
onRemove: Ai.attachFile("")
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout { // Input field and send button
|
RowLayout { // Input field and send button
|
||||||
id: inputFieldRowLayout
|
id: inputFieldRowLayout
|
||||||
anchors.top: parent.top
|
anchors {
|
||||||
anchors.left: parent.left
|
top: attachedFileIndicator.bottom
|
||||||
anchors.right: parent.right
|
left: parent.left
|
||||||
anchors.topMargin: 5
|
right: parent.right
|
||||||
|
topMargin: 5
|
||||||
|
}
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
StyledTextArea { // The actual TextArea
|
StyledTextArea { // The actual TextArea
|
||||||
|
|||||||
@@ -233,6 +233,15 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
active: root.messageData?.localFilePath && root.messageData?.localFilePath.length > 0
|
||||||
|
sourceComponent: AttachedFileIndicator {
|
||||||
|
filePath: root.messageData?.localFilePath
|
||||||
|
canRemove: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout { // Message content
|
ColumnLayout { // Message content
|
||||||
id: messageContentColumnLayout
|
id: messageContentColumnLayout
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import qs.modules.common.widgets
|
import qs.modules.common.widgets
|
||||||
import qs.services
|
|
||||||
import qs.modules.common.functions
|
import qs.modules.common.functions
|
||||||
|
import qs.services
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell.Hyprland
|
import Quickshell.Hyprland
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs
|
||||||
|
import qs.modules.common
|
||||||
|
import qs.modules.common.widgets
|
||||||
|
import qs.services
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal remove()
|
||||||
|
property bool canRemove: true
|
||||||
|
property string filePath: ""
|
||||||
|
property string mimeType: ""
|
||||||
|
property real maxHeight: 200
|
||||||
|
property real imageWidth: -1
|
||||||
|
property real imageHeight: -1
|
||||||
|
property real scale: Math.min(root.maxHeight / imageHeight, root.width / imageWidth)
|
||||||
|
onFilePathChanged: refresh()
|
||||||
|
visible: filePath !== ""
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
root.mimeType = "";
|
||||||
|
root.imageWidth = -1;
|
||||||
|
root.imageHeight = -1;
|
||||||
|
fileTypeProc.exec(["file", "-b", "--mime-type", filePath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fileTypeProc
|
||||||
|
command: ["file", "-b", "--mime-type", filePath]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.mimeType = this.text;
|
||||||
|
if (root.mimeType.startsWith("image/"))
|
||||||
|
imageSizeProc.exec(["identify", "-format", "%wx%h", filePath]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: imageSizeProc
|
||||||
|
command: ["identify", "-format", "%wx%h", filePath]
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
const dimensions = this.text.split("x");
|
||||||
|
root.imageWidth = parseInt(dimensions[0]);
|
||||||
|
root.imageHeight = parseInt(dimensions[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Styles/widgets
|
||||||
|
property real horizontalPadding: 10
|
||||||
|
property real verticalPadding: 10
|
||||||
|
radius: Appearance.rounding.small - anchors.margins
|
||||||
|
color: Appearance.colors.colLayer2
|
||||||
|
implicitHeight: visible ? (contentItem.implicitHeight + verticalPadding * 2) : 0
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: contentItem
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
leftMargin: root.horizontalPadding
|
||||||
|
rightMargin: root.horizontalPadding
|
||||||
|
topMargin: root.verticalPadding
|
||||||
|
bottomMargin: root.verticalPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
MaterialSymbol {
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
text: {
|
||||||
|
if (root.mimeType.startsWith("image/"))
|
||||||
|
return "image";
|
||||||
|
if (root.mimeType.startsWith("audio/"))
|
||||||
|
return "music_note";
|
||||||
|
if (root.mimeType.startsWith("video/"))
|
||||||
|
return "movie";
|
||||||
|
if (root.mimeType === "application/pdf")
|
||||||
|
return "picture_as_pdf";
|
||||||
|
if (root.mimeType.startsWith("text/"))
|
||||||
|
return "description";
|
||||||
|
return "file_present";
|
||||||
|
}
|
||||||
|
iconSize: Appearance.font.pixelSize.hugeass
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 4
|
||||||
|
text: root.filePath
|
||||||
|
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||||
|
font.family: Appearance.font.family.monospace
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
RippleButton {
|
||||||
|
visible: root.canRemove
|
||||||
|
Layout.alignment: Qt.AlignTop
|
||||||
|
buttonRadius: Appearance.rounding.full
|
||||||
|
colBackground: Appearance.colors.colLayer2
|
||||||
|
implicitHeight: 28
|
||||||
|
implicitWidth: 28
|
||||||
|
contentItem: MaterialSymbol {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "close"
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
iconSize: Appearance.font.pixelSize.larger
|
||||||
|
color: Appearance.colors.colOnSurfaceVariant
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: root.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: imagePreviewLoader
|
||||||
|
visible: (root.imageWidth != -1) && (root.imageHeight != -1)
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
sourceComponent: Item {
|
||||||
|
implicitHeight: root.imageHeight * root.scale
|
||||||
|
implicitWidth: imagePreview.implicitWidth
|
||||||
|
Image {
|
||||||
|
id: imagePreview
|
||||||
|
anchors.fill: parent
|
||||||
|
source: Qt.resolvedUrl(root.filePath)
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
antialiasing: true
|
||||||
|
asynchronous: true
|
||||||
|
width: root.imageWidth * root.scale
|
||||||
|
height: root.imageHeight * root.scale
|
||||||
|
sourceSize.width: root.imageWidth * root.scale
|
||||||
|
sourceSize.height: root.imageHeight * root.scale
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: Rectangle {
|
||||||
|
width: imagePreview.width
|
||||||
|
height: imagePreview.height
|
||||||
|
radius: Appearance.rounding.normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -368,6 +368,9 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property string requestScriptFilePath: "/tmp/quickshell/ai/request.sh"
|
||||||
|
property string pendingFilePath: ""
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
setModel(currentModelId, false, false); // Do necessary setup for model
|
setModel(currentModelId, false, false); // Do necessary setup for model
|
||||||
}
|
}
|
||||||
@@ -617,9 +620,13 @@ Singleton {
|
|||||||
root.tokenCount.total = -1;
|
root.tokenCount.total = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: requesterScriptFile
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: requester
|
id: requester
|
||||||
property list<string> baseCommand: ["bash", "-c"]
|
property list<string> baseCommand: ["bash"]
|
||||||
property AiMessageData message
|
property AiMessageData message
|
||||||
property ApiStrategy currentStrategy
|
property ApiStrategy currentStrategy
|
||||||
|
|
||||||
@@ -645,7 +652,7 @@ Singleton {
|
|||||||
const endpoint = root.currentApiStrategy.buildEndpoint(model);
|
const endpoint = root.currentApiStrategy.buildEndpoint(model);
|
||||||
const messageArray = root.messageIDs.map(id => root.messageByID[id]);
|
const messageArray = root.messageIDs.map(id => root.messageByID[id]);
|
||||||
const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole);
|
const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole);
|
||||||
const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature, root.tools[model.api_format][root.currentTool]);
|
const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature, root.tools[model.api_format][root.currentTool], root.pendingFilePath);
|
||||||
// console.log("[Ai] Request data: ", JSON.stringify(data, null, 2));
|
// console.log("[Ai] Request data: ", JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
let requestHeaders = {
|
let requestHeaders = {
|
||||||
@@ -677,14 +684,31 @@ Singleton {
|
|||||||
/* Get authorization header from strategy */
|
/* Get authorization header from strategy */
|
||||||
const authHeader = requester.currentStrategy.buildAuthorizationHeader(root.apiKeyEnvVarName);
|
const authHeader = requester.currentStrategy.buildAuthorizationHeader(root.apiKeyEnvVarName);
|
||||||
|
|
||||||
|
/* Script shebang */
|
||||||
|
const scriptShebang = "#!/usr/bin/env bash\n";
|
||||||
|
|
||||||
|
/* Create extra setup when there's an attached file */
|
||||||
|
let scriptFileSetupContent = ""
|
||||||
|
if (root.pendingFilePath && root.pendingFilePath.length > 0) {
|
||||||
|
requester.message.localFilePath = root.pendingFilePath;
|
||||||
|
scriptFileSetupContent = requester.currentStrategy.buildScriptFileSetup(root.pendingFilePath);
|
||||||
|
root.pendingFilePath = ""
|
||||||
|
}
|
||||||
|
|
||||||
/* Create command string */
|
/* Create command string */
|
||||||
const requestCommandString = `curl --no-buffer "${endpoint}"`
|
let scriptRequestContent = ""
|
||||||
|
scriptRequestContent += `curl --no-buffer "${endpoint}"`
|
||||||
+ ` ${headerString}`
|
+ ` ${headerString}`
|
||||||
+ (authHeader ? ` ${authHeader}` : "")
|
+ (authHeader ? ` ${authHeader}` : "")
|
||||||
+ ` -d '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'`
|
+ ` --data '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'`
|
||||||
|
+ "\n"
|
||||||
|
|
||||||
/* Send the request */
|
/* Send the request */
|
||||||
requester.command = baseCommand.concat([requestCommandString]);
|
const scriptContent = requester.currentStrategy.finalizeScriptContent(scriptShebang + scriptFileSetupContent + scriptRequestContent)
|
||||||
|
const shellScriptPath = CF.FileUtils.trimFileProtocol(root.requestScriptFilePath)
|
||||||
|
requesterScriptFile.path = Qt.resolvedUrl(shellScriptPath)
|
||||||
|
requesterScriptFile.setText(scriptContent)
|
||||||
|
requester.command = baseCommand.concat([shellScriptPath]);
|
||||||
requester.running = true
|
requester.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,6 +766,10 @@ Singleton {
|
|||||||
requester.makeRequest();
|
requester.makeRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function attachFile(filePath: string) {
|
||||||
|
root.pendingFilePath = CF.FileUtils.trimFileProtocol(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
function createFunctionOutputMessage(name, output, includeOutputInChat = true) {
|
function createFunctionOutputMessage(name, output, includeOutputInChat = true) {
|
||||||
return aiMessageComponent.createObject(root, {
|
return aiMessageComponent.createObject(root, {
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -841,6 +869,9 @@ Singleton {
|
|||||||
return ({
|
return ({
|
||||||
"role": message.role,
|
"role": message.role,
|
||||||
"rawContent": message.rawContent,
|
"rawContent": message.rawContent,
|
||||||
|
"fileMimeType": message.fileMimeType,
|
||||||
|
"fileUri": message.fileUri,
|
||||||
|
"localFilePath": message.localFilePath,
|
||||||
"model": message.model,
|
"model": message.model,
|
||||||
"thinking": false,
|
"thinking": false,
|
||||||
"done": true,
|
"done": true,
|
||||||
@@ -858,7 +889,7 @@ Singleton {
|
|||||||
id: chatSaveFile
|
id: chatSaveFile
|
||||||
property string chatName: "chat"
|
property string chatName: "chat"
|
||||||
path: `${Directories.aiChats}/${chatName}.json`
|
path: `${Directories.aiChats}/${chatName}.json`
|
||||||
blockLoading: true
|
blockLoading: true // Prevent race conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -894,6 +925,9 @@ Singleton {
|
|||||||
"role": message.role,
|
"role": message.role,
|
||||||
"rawContent": message.rawContent,
|
"rawContent": message.rawContent,
|
||||||
"content": message.rawContent,
|
"content": message.rawContent,
|
||||||
|
"fileMimeType": message.fileMimeType,
|
||||||
|
"fileUri": message.fileUri,
|
||||||
|
"localFilePath": message.localFilePath,
|
||||||
"model": message.model,
|
"model": message.model,
|
||||||
"thinking": message.thinking,
|
"thinking": message.thinking,
|
||||||
"done": message.done,
|
"done": message.done,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ QtObject {
|
|||||||
property string role
|
property string role
|
||||||
property string content
|
property string content
|
||||||
property string rawContent
|
property string rawContent
|
||||||
|
property string fileMimeType
|
||||||
|
property string fileUri
|
||||||
|
property string localFilePath
|
||||||
property string model
|
property string model
|
||||||
property bool thinking: true
|
property bool thinking: true
|
||||||
property bool done: false
|
property bool done: false
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import QtQuick
|
|||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
function buildEndpoint(model: AiModel): string { throw new Error("Not implemented") }
|
function buildEndpoint(model: AiModel): string { throw new Error("Not implemented") }
|
||||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) { throw new Error("Not implemented") }
|
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>, filePath: string) { throw new Error("Not implemented") }
|
||||||
function buildAuthorizationHeader(apiKeyEnvVarName: string): string { throw new Error("Not implemented") }
|
function buildAuthorizationHeader(apiKeyEnvVarName: string): string { throw new Error("Not implemented") }
|
||||||
function parseResponseLine(line: string, message: AiMessageData) { throw new Error("Not implemented") }
|
function parseResponseLine(line: string, message: AiMessageData) { throw new Error("Not implemented") }
|
||||||
function onRequestFinished(message: AiMessageData): var { return {} } // Default: no special handling
|
function onRequestFinished(message: AiMessageData): var { return {} } // Default: no special handling
|
||||||
function reset() { } // Reset any internal state if needed
|
function reset() { } // Reset any internal state if needed
|
||||||
|
function buildScriptFileSetup(filePath) { return "" } // Default: no setup
|
||||||
|
function finalizeScriptContent(scriptContent: string): string { return scriptContent } // Optionally modify/finalize script
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
|
import qs.modules.common.functions as CF
|
||||||
|
|
||||||
ApiStrategy {
|
ApiStrategy {
|
||||||
|
readonly property string apiKeyEnvVarName: "API_KEY"
|
||||||
|
readonly property string fileUriVarName: "file_uri"
|
||||||
|
readonly property string fileMimeTypeVarName: "MIME_TYPE"
|
||||||
|
readonly property string fileUriSubstitutionString: "{{ fileUriVarName }}"
|
||||||
|
readonly property string fileMimeTypeSubstitutionString: "{{ fileMimeTypeVarName }}"
|
||||||
property string buffer: ""
|
property string buffer: ""
|
||||||
|
|
||||||
function buildEndpoint(model: AiModel): string {
|
function buildEndpoint(model: AiModel): string {
|
||||||
@@ -9,39 +15,57 @@ ApiStrategy {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) {
|
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>, filePath: string) {
|
||||||
let baseData = {
|
let contents = messages.map(message => {
|
||||||
"contents": messages.map(message => {
|
// console.log("[AI] Building request data for message:", JSON.stringify(message, null, 2));
|
||||||
const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role;
|
const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role;
|
||||||
const usingSearch = tools[0]?.google_search !== undefined
|
const usingSearch = tools[0]?.google_search !== undefined
|
||||||
if (!usingSearch && message.functionCall != undefined && message.functionName.length > 0) {
|
if (!usingSearch && message.functionCall != undefined && message.functionName.length > 0) {
|
||||||
return {
|
|
||||||
"role": geminiApiRoleName,
|
|
||||||
"parts": [{
|
|
||||||
functionCall: {
|
|
||||||
"name": message.functionName,
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!usingSearch && message.functionResponse != undefined && message.functionName.length > 0) {
|
|
||||||
return {
|
|
||||||
"role": geminiApiRoleName,
|
|
||||||
"parts": [{
|
|
||||||
functionResponse: {
|
|
||||||
"name": message.functionName,
|
|
||||||
"response": { "content": message.functionResponse }
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
"role": geminiApiRoleName,
|
"role": geminiApiRoleName,
|
||||||
"parts": [{
|
"parts": [{
|
||||||
text: message.rawContent,
|
functionCall: {
|
||||||
|
"name": message.functionName,
|
||||||
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
if (!usingSearch && message.functionResponse != undefined && message.functionName.length > 0) {
|
||||||
|
return {
|
||||||
|
"role": geminiApiRoleName,
|
||||||
|
"parts": [{
|
||||||
|
functionResponse: {
|
||||||
|
"name": message.functionName,
|
||||||
|
"response": { "content": message.functionResponse }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"role": geminiApiRoleName,
|
||||||
|
"parts": [
|
||||||
|
{ text: message.rawContent },
|
||||||
|
...(message.fileUri && message.fileUri.length > 0 ? [{
|
||||||
|
"file_data": {
|
||||||
|
"mime_type": message.fileMimeType,
|
||||||
|
"file_uri": message.fileUri
|
||||||
|
}
|
||||||
|
}] : [])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (filePath && filePath.length > 0) {
|
||||||
|
const trimmedFilePath = CF.FileUtils.trimFileProtocol(filePath);
|
||||||
|
// Add file_data part to the last message's parts array
|
||||||
|
contents[contents.length - 1].parts.unshift({
|
||||||
|
file_data: {
|
||||||
|
mime_type: fileMimeTypeSubstitutionString,
|
||||||
|
file_uri: fileUriSubstitutionString
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let baseData = {
|
||||||
|
"contents": contents,
|
||||||
"tools": tools,
|
"tools": tools,
|
||||||
"system_instruction": {
|
"system_instruction": {
|
||||||
"parts": [{ text: systemPrompt }]
|
"parts": [{ text: systemPrompt }]
|
||||||
@@ -50,6 +74,7 @@ ApiStrategy {
|
|||||||
"temperature": temperature,
|
"temperature": temperature,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// print("Gemini API call payload:", JSON.stringify(baseData, null, 2));
|
||||||
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
|
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,8 +103,18 @@ ApiStrategy {
|
|||||||
try {
|
try {
|
||||||
if (buffer.length === 0) return {};
|
if (buffer.length === 0) return {};
|
||||||
const dataJson = JSON.parse(buffer);
|
const dataJson = JSON.parse(buffer);
|
||||||
|
|
||||||
|
// Uploaded file
|
||||||
|
if (dataJson.uploadedFile) {
|
||||||
|
message.fileUri = dataJson.uploadedFile.uri;
|
||||||
|
message.fileMimeType = dataJson.uploadedFile.mimeType;
|
||||||
|
return ({})
|
||||||
|
}
|
||||||
|
|
||||||
|
// No candidates?
|
||||||
if (!dataJson.candidates) return {};
|
if (!dataJson.candidates) return {};
|
||||||
|
|
||||||
|
// Finished?
|
||||||
if (dataJson.candidates[0]?.finishReason) {
|
if (dataJson.candidates[0]?.finishReason) {
|
||||||
finished = true;
|
finished = true;
|
||||||
}
|
}
|
||||||
@@ -152,4 +187,55 @@ ApiStrategy {
|
|||||||
function reset() {
|
function reset() {
|
||||||
buffer = "";
|
buffer = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildScriptFileSetup(filePath) {
|
||||||
|
const trimmedFilePath = CF.FileUtils.trimFileProtocol(filePath);
|
||||||
|
let content = ""
|
||||||
|
|
||||||
|
// print("file path:", filePath)
|
||||||
|
// print("trimmed file path:", trimmedFilePath)
|
||||||
|
// print("escaped file path:", CF.StringUtils.shellSingleQuoteEscape(trimmedFilePath))
|
||||||
|
|
||||||
|
content += `IMAGE_PATH='${CF.StringUtils.shellSingleQuoteEscape(trimmedFilePath)}'\n`;
|
||||||
|
content += `${fileMimeTypeVarName}=$(file -b --mime-type "$IMAGE_PATH")\n`;
|
||||||
|
content += 'NUM_BYTES=$(wc -c < "${IMAGE_PATH}")\n';
|
||||||
|
content += 'tmp_header_file="/tmp/quickshell/ai/upload-header.tmp"\n';
|
||||||
|
content += 'tmp_file_info_file="/tmp/quickshell/ai/file-info.json.tmp"\n';
|
||||||
|
|
||||||
|
// Initial resumable request defining metadata.
|
||||||
|
// The upload url is in the response headers dump them to a file.
|
||||||
|
content += 'curl "https://generativelanguage.googleapis.com/upload/v1beta/files"'
|
||||||
|
+ ` -H "x-goog-api-key: \$${apiKeyEnvVarName}"`
|
||||||
|
+ ' -D $tmp_header_file'
|
||||||
|
+ ' -H "X-Goog-Upload-Protocol: resumable"'
|
||||||
|
+ ' -H "X-Goog-Upload-Command: start"'
|
||||||
|
+ ' -H "X-Goog-Upload-Header-Content-Length: ${NUM_BYTES}"'
|
||||||
|
+ ` -H "X-Goog-Upload-Header-Content-Type: \${${fileMimeTypeVarName}}"`
|
||||||
|
+ ' -H "Content-Type: application/json"'
|
||||||
|
+ ` -d "{'file': {'display_name': 'Image'}}" 2> /dev/null`
|
||||||
|
+ '\n';
|
||||||
|
|
||||||
|
// Get file upload header
|
||||||
|
content += 'upload_url=$(grep -i "x-goog-upload-url: " "${tmp_header_file}" | cut -d" " -f2 | tr -d "\r")\n';
|
||||||
|
content += 'rm "${tmp_header_file}"\n';
|
||||||
|
|
||||||
|
// Upload the actual file
|
||||||
|
content += 'curl "${upload_url}"'
|
||||||
|
+ ` -H "x-goog-api-key: \$${apiKeyEnvVarName}"`
|
||||||
|
+ ' -H "Content-Length: ${NUM_BYTES}"'
|
||||||
|
+ ' -H "X-Goog-Upload-Offset: 0"'
|
||||||
|
+ ' -H "X-Goog-Upload-Command: upload, finalize"'
|
||||||
|
+ ' --data-binary "@${IMAGE_PATH}" 2> /dev/null > "${tmp_file_info_file}"'
|
||||||
|
+ '\n';
|
||||||
|
|
||||||
|
content += `${fileUriVarName}=$(jq -r ".file.uri" "$tmp_file_info_file")\n`
|
||||||
|
content += `printf "{\\"uploadedFile\\": {\\"uri\\": \\"$${fileUriVarName}\\", \\"mimeType\\": \\"$${fileMimeTypeVarName}\\"}}\\n,\\n"\n`
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeScriptContent(scriptContent: string): string {
|
||||||
|
return scriptContent.replace(fileMimeTypeSubstitutionString, `'"\$${fileMimeTypeVarName}"'`)
|
||||||
|
.replace(fileUriSubstitutionString, `'"\$${fileUriVarName}"'`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ApiStrategy {
|
|||||||
return model.endpoint;
|
return model.endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) {
|
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>, filePath: string) {
|
||||||
let baseData = {
|
let baseData = {
|
||||||
"model": model.model,
|
"model": model.model,
|
||||||
"messages": [
|
"messages": [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ ApiStrategy {
|
|||||||
return model.endpoint;
|
return model.endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>) {
|
function buildRequestData(model: AiModel, messages, systemPrompt: string, temperature: real, tools: list<var>, filePath: string) {
|
||||||
let baseData = {
|
let baseData = {
|
||||||
"model": model.model,
|
"model": model.model,
|
||||||
"messages": [
|
"messages": [
|
||||||
|
|||||||
Reference in New Issue
Block a user