From 9ea833d40e73ef6b46dd249f21ecf9a5eeb3a183 Mon Sep 17 00:00:00 2001 From: dasein Date: Sun, 30 Jun 2024 09:43:02 -0300 Subject: [PATCH] feat(rune): extend soul companion with openai --- docs/scripting.md | 7 +- package.json | 2 +- .../SoulCompanion/SoulCompanion.tsx | 34 +++---- .../SoulCompanion/soulCompanion.module.scss | 5 +- src/contexts/scripting/scripting.tsx | 42 +++++---- src/features/ipfs/Drive/BackendStatus.tsx | 2 +- src/redux/reducers/scripting.ts | 2 +- src/services/scripting/engine.ts | 13 ++- src/services/scripting/helpers.ts | 3 + .../scripting/rune/default/particle.rn | 20 ++++- .../scripting/services/llmRequests/openai.ts | 89 ++++++++++++++----- src/services/scripting/types.ts | 2 +- src/services/scripting/wasmBindings.js | 7 +- yarn.lock | 8 +- 14 files changed, 157 insertions(+), 79 deletions(-) diff --git a/docs/scripting.md b/docs/scripting.md index 765d42546..0b0c78715 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -85,11 +85,14 @@ cyb::search_by_embedding(text:string, count: int) -> string[]; #### Experemental -OpenAI promts(beta, in developement) +OpenAI promts(beta) + +- api key should be added using cyb-keys +- this is wrapper around [openai api](https://platform.openai.com/docs/api-reference/chat/create) ``` // Apply prompt OpenAI and get result -cyb::open_ai_prompt(prompt: string; params: json) -> string; +cyb::open_ai_completions(messages: object[]; apiKey: string; params: json) -> string | AsyncIterable; ``` #### Debug diff --git a/package.json b/package.json index 63abc6d14..4b1b50e55 100644 --- a/package.json +++ b/package.json @@ -192,7 +192,7 @@ "core-js": "^3.30.0", "crypto": "^1.0.1", "cyb-cozo-lib-wasm": "^0.7.145", - "cyb-rune-wasm": "^0.0.84", + "cyb-rune-wasm": "^0.0.841", "datastore-core": "^9.2.3", "datastore-idb": "^2.1.4", "dateformat": "^3.0.3", diff --git a/src/containers/ipfs/components/SoulCompanion/SoulCompanion.tsx b/src/containers/ipfs/components/SoulCompanion/SoulCompanion.tsx index 9a8f4afbb..04d4d79d6 100644 --- a/src/containers/ipfs/components/SoulCompanion/SoulCompanion.tsx +++ b/src/containers/ipfs/components/SoulCompanion/SoulCompanion.tsx @@ -34,7 +34,7 @@ function SoulCompanion({ if (details.type && details.type !== 'text' && details.text) { setStatus('done'); setMetaItems([ - { type: 'text', text: `Skip companion for '${details.content}'.` }, + [{ type: 'text', text: `Skip companion for '${details.content}'.` }], ]); return; } @@ -65,21 +65,23 @@ function SoulCompanion({ ); } return ( -
-
    - {metaItems.map((item, index) => ( -
  • - {item.type === 'text' && ( -

    {item.text}

    - )} - {item.type === 'link' && ( - - {shortenString(item.title, 64)} - - )} -
  • - ))} -
+
+ {metaItems.map((row, index) => ( +
    + {row.map((item, index) => ( +
  • + {item.type === 'text' && ( +

    {item.text}

    + )} + {item.type === 'link' && ( + + {shortenString(item.title, 64)} + + )} +
  • + ))} +
+ ))}
); } diff --git a/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss b/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss index d65acd3fa..ba4d32dab 100644 --- a/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss +++ b/src/containers/ipfs/components/SoulCompanion/soulCompanion.module.scss @@ -1,11 +1,14 @@ .itemLinks { display: flex; - margin: -10px 0; list-style-type: none; font-size: 14px; } +.soulCompanion { + margin: -10px 0; +} + .itemText { font-size: 14px; display: block; diff --git a/src/contexts/scripting/scripting.tsx b/src/contexts/scripting/scripting.tsx index 418093e7f..fae0a08d4 100644 --- a/src/contexts/scripting/scripting.tsx +++ b/src/contexts/scripting/scripting.tsx @@ -45,15 +45,17 @@ function ScriptingProvider({ children }: { children: React.ReactNode }) { const dispatch = useAppDispatch(); useEffect(() => { + runeBackend.pushContext('secrets', secrets); + const setupObservervable = async () => { const { isSoulInitialized$ } = runeBackend; const soulSubscription = (await isSoulInitialized$).subscribe((v) => { - setIsSoulInitialized(!!v); if (v) { runeRef.current = runeBackend; console.log('👻 soul initalized'); } + setIsSoulInitialized(!!v); }); const embeddingApiSubscription = (await embeddingApi$).subscribe( @@ -72,28 +74,38 @@ function ScriptingProvider({ children }: { children: React.ReactNode }) { }; setupObservervable(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const runeEntryPoints = useAppSelector(selectRuneEntypoints); const citizenship = useAppSelector(selectCurrentPassport); + const secrets = useAppSelector((state) => state.scripting.context.secrets); useEffect(() => { - (async () => { - if (citizenship) { - const particleCid = citizenship.extension.particle; + if (!isSoulInitialized || !runeRef.current) { + return; + } + + if (citizenship) { + const particleCid = citizenship.extension.particle; + + runeRef.current.pushContext('user', { + address: citizenship.owner, + nickname: citizenship.extension.nickname, + citizenship, + particle: particleCid, + } as UserContext); + } else { + runeRef.current.popContext(['user']); + } + }, [citizenship, isSoulInitialized]); - await runeBackend.pushContext('user', { - address: citizenship.owner, - nickname: citizenship.extension.nickname, - citizenship, - particle: particleCid, - } as UserContext); - } else { - await runeBackend.popContext(['user', 'secrets']); - } - })(); - }, [citizenship, runeBackend]); + useEffect(() => { + if (isSoulInitialized && runeRef.current) { + runeRef.current.pushContext('secrets', secrets); + } + }, [secrets, isSoulInitialized]); useEffect(() => { (async () => { diff --git a/src/features/ipfs/Drive/BackendStatus.tsx b/src/features/ipfs/Drive/BackendStatus.tsx index d2f155340..d810f8cce 100644 --- a/src/features/ipfs/Drive/BackendStatus.tsx +++ b/src/features/ipfs/Drive/BackendStatus.tsx @@ -19,7 +19,7 @@ import { downloadJson } from 'src/utils/json'; import { useBackend } from 'src/contexts/backend/backend'; import { EmbeddinsDbEntity } from 'src/services/CozoDb/types/entities'; import { isObject } from 'lodash'; -import { promptToOpenAI } from 'src/services/scripting/services/llmRequests/openai'; +import { openAICompletion } from 'src/services/scripting/services/llmRequests/openai'; const getProgressTrackingInfo = (progress?: ProgressTracking) => { if (!progress) { diff --git a/src/redux/reducers/scripting.ts b/src/redux/reducers/scripting.ts index fc168da2a..2cb1dcb67 100644 --- a/src/redux/reducers/scripting.ts +++ b/src/redux/reducers/scripting.ts @@ -56,7 +56,7 @@ const initialScriptEntrypoints: ScriptEntrypoints = { const initialState: SliceState = { context: { - secrets: loadJsonFromLocalStorage('secrets', {}) as TabularKeyValues, + secrets: loadJsonFromLocalStorage('secrets', {}), params: {}, user: {}, }, diff --git a/src/services/scripting/engine.ts b/src/services/scripting/engine.ts index c4c2d1fb0..225744b4a 100644 --- a/src/services/scripting/engine.ts +++ b/src/services/scripting/engine.ts @@ -105,13 +105,9 @@ function enigine() { const pushContext = ( name: K, - value: ScriptContext[K] | TabularKeyValues + value: ScriptContext[K] //| TabularKeyValues ) => { - if (name === 'secrets') { - context[name] = toRecord(value as TabularKeyValues); - return; - } - + // context[name] = toRecord(value as TabularKeyValues); context[name] = value; }; @@ -151,6 +147,7 @@ function enigine() { params: scriptParams, }; + // console.log('-----run', scriptParams); const outputData = await compile(compilerParams, compileConfig); // Parse the JSON string @@ -270,7 +267,7 @@ function enigine() { if (resultType === 'error') { return { action: 'error', - metaItems: [{ type: 'text', text: 'No particle entrypoint' }], + metaItems: [[{ type: 'text', text: 'No particle entrypoint' }]], }; } @@ -291,7 +288,7 @@ function enigine() { console.error('---askCompanion error', output); return { action: 'error', - metaItems: [{ type: 'text', text: output.error }], + metaItems: [[{ type: 'text', text: output.error }]], }; } diff --git a/src/services/scripting/helpers.ts b/src/services/scripting/helpers.ts index cceed14c1..6e4eeecee 100644 --- a/src/services/scripting/helpers.ts +++ b/src/services/scripting/helpers.ts @@ -1,4 +1,5 @@ import { Nullable } from 'src/types'; +import { v4 as uuidv4 } from 'uuid'; export async function getScriptFromParticle(cid?: Nullable) { throw new Error('Not implemented'); @@ -50,3 +51,5 @@ export function extractRuneScript(markdown: string) { // if no rune tag, consider this like pure script return hasRune ? script : md; } + +export const generateRefId = () => uuidv4().toString(); diff --git a/src/services/scripting/rune/default/particle.rn b/src/services/scripting/rune/default/particle.rn index 1742d805f..c4f10173c 100644 --- a/src/services/scripting/rune/default/particle.rn +++ b/src/services/scripting/rune/default/particle.rn @@ -23,6 +23,7 @@ pub async fn moon_domain_resolver() { pub async fn ask_companion(cid, content_type, content) { // plain text item let links = [meta_text("similar: ")]; + let rows = [links]; // search closest 5 particles using local data from the brain let similar_results = cyb::search_by_embedding(content, 5).await; @@ -37,7 +38,24 @@ pub async fn ask_companion(cid, content_type, content) { links = [meta_text("no similar particles found")]; } - return content_result(links) + let secrets = cyb::context.secrets; + if let Some(api_key) = secrets.get("openAI_key") { + let messages = [ + #{ + "role": "system", + "content": "You should give description or summary of any content. aswer should not exceed 32 words" + }, + #{ + "role": "user", + "content": content + } + ]; + + let inference = cyb::open_ai_completions(messages, api_key, #{"model": "gpt-3.5-turbo"}).await; + rows.push([meta_text(`inference: ${inference}`)]); + } + + return content_result(rows) } // Transform content of the particle diff --git a/src/services/scripting/services/llmRequests/openai.ts b/src/services/scripting/services/llmRequests/openai.ts index 252275721..1e86491db 100644 --- a/src/services/scripting/services/llmRequests/openai.ts +++ b/src/services/scripting/services/llmRequests/openai.ts @@ -1,35 +1,76 @@ /* eslint-disable import/prefer-default-export */ /* eslint-disable import/no-unused-modules */ -import axios from 'axios'; +import axios, { ResponseType } from 'axios'; // https://platform.openai.com/docs/models/overview // gpt-3.5-turbo -// https://platform.openai.com/docs/api-reference/chat/create -export const promptToOpenAI = async ( - prompt: string, +type OpenAiMessage = { + role: 'system' | 'user' | 'assistant'; + content: string; +}; + +interface OpenAIParams { + model: string; + messages: OpenAiMessage[]; + [key: string]: any; +} + +const defaultOpenAIParams: Partial = { + model: 'gpt-3.5-turbo', +}; + +export const openAICompletion = async ( + messages: OpenAiMessage[], apiKey: string, - params: any = { - model: 'text-davinci-003', // 'gpt-3.5-turbo', - maxTokens: 500, - stop: '.', - n: 1, - } -) => { - //prompt: `Complete this sentence: "${input}"`, - const response = await axios.post( - 'https://api.openai.com/v1/completions', - { - prompt, + params: Partial = {} +): Promise> => { + const requestOptions = { + method: 'post', + url: 'https://api.openai.com/v1/chat/completions', + data: { + messages, + ...defaultOpenAIParams, ...params, }, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + responseType: (params.stream ? 'stream' : 'json') as ResponseType, + }; + + const response = await axios(requestOptions); + + if (!params.stream) { + // Non-streaming request + console.log('response', response); + return response.data.choices[0].message.content; + } else { + // Streaming request + const asyncIterable: AsyncIterable = { + [Symbol.asyncIterator]: async function* () { + let result = ''; + for await (const chunk of response.data) { + const str = chunk.toString(); + const lines = str.split('\n').filter((line) => line.trim() !== ''); + for (const line of lines) { + const message = line.replace(/^data: /, ''); + if (message === '[DONE]') { + return; + } + try { + const parsed = JSON.parse(message); + result += parsed.choices[0].delta.content; + yield parsed.choices[0].delta.content; + } catch (error) { + console.error('Error parsing stream message:', message, error); + } + } + } }, - } - ); - console.log('response', response); - return response.data.choices[0].text; + }; + + return asyncIterable; + } }; diff --git a/src/services/scripting/types.ts b/src/services/scripting/types.ts index 47f77ea8a..8d44431d6 100644 --- a/src/services/scripting/types.ts +++ b/src/services/scripting/types.ts @@ -76,7 +76,7 @@ type MetaTextComponent = { type ScriptMyCampanion = { action: 'pass' | 'answer' | 'error'; - metaItems: (MetaLinkComponent | MetaTextComponent)[]; + metaItems: (MetaLinkComponent | MetaTextComponent)[][]; }; // type ScriptScopeParams = { diff --git a/src/services/scripting/wasmBindings.js b/src/services/scripting/wasmBindings.js index 2f09960a6..94d034c83 100644 --- a/src/services/scripting/wasmBindings.js +++ b/src/services/scripting/wasmBindings.js @@ -1,7 +1,7 @@ /* eslint-disable import/no-unused-modules */ import { getFromLink, getToLink } from 'src/utils/search/utils'; import runeDeps from './runeDeps'; -import { promptToOpenAI } from './services/llmRequests/openai'; +import { openAICompletion } from './services/llmRequests/openai'; // let runeDeps; @@ -34,9 +34,8 @@ export async function jsAddContenToIpfs(content) { return runeDeps.addContenToIpfs(content); } -export async function jsPromptToOpenAI(prompt, apiKey, params, refId) { - console.log('jsPromptToOpenAI', prompt, apiKey, params, refId); - const result = await promptToOpenAI(prompt, apiKey, params); +export async function jsOpenAICompletions(messages, apiKey, params, refId) { + const result = await openAICompletion(messages, apiKey, params); return result; } diff --git a/yarn.lock b/yarn.lock index 0a3c28fd9..84d6f9f6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14592,10 +14592,10 @@ cyb-cozo-lib-wasm@^0.7.145: resolved "https://registry.yarnpkg.com/cyb-cozo-lib-wasm/-/cyb-cozo-lib-wasm-0.7.145.tgz#df82255d478415134d0a2cf82de1467c2009db92" integrity sha512-vCbiFuBPFOaMyS9kPutatBDekAqEafmsQ9jX34J3eMzQiBmoyujsm+z6jZiLwVWz2yDNtF/H17ofgexalHKjnw== -cyb-rune-wasm@^0.0.84: - version "0.0.84" - resolved "https://registry.yarnpkg.com/cyb-rune-wasm/-/cyb-rune-wasm-0.0.84.tgz#c5a4e8e2f38612b0f9f8cedf9e5014de7deb5a8d" - integrity sha512-9nNHXw8sC5IOxmGLEyjdebxaIEio6fcErZV7i6z4mm+z3yrlVLaSP+67Bun0WtGgyi6IYWhkwUCaou7k36/MDg== +cyb-rune-wasm@^0.0.841: + version "0.0.841" + resolved "https://registry.yarnpkg.com/cyb-rune-wasm/-/cyb-rune-wasm-0.0.841.tgz#048402b9ff0fe0c82f9d03e1b90a60847279b6ac" + integrity sha512-+gLp8Hif/STVItVCeE3vNIKh9JJsXLh4dyZiU4V9QYBKmCdOsfc5ZkUFsOQmhvT9jORQx8oLQRVPmBMhptPORg== d3-array@1, d3-array@^1.1.1, d3-array@^1.2.0, d3-array@^1.2.1: version "1.2.4"