mirror of
https://github.com/wassname/chatGPTBox.git
synced 2026-07-01 20:21:51 +08:00
custom npm claude-ai
This commit is contained in:
Generated
-61
@@ -10,7 +10,6 @@
|
||||
"@picocss/pico": "^1.5.9",
|
||||
"@primer/octicons-react": "^18.3.0",
|
||||
"buffer": "^6.0.3",
|
||||
"claude-ai": "^1.2.2",
|
||||
"countries-list": "^2.6.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"diff": "^5.1.0",
|
||||
@@ -3431,14 +3430,6 @@
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/claude-ai": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/claude-ai/-/claude-ai-1.2.2.tgz",
|
||||
"integrity": "sha512-/4NWVJvlT5+jiFxuXq6HUxFW2bN3fyaXbL2cJNF1u5vpNE/C4WkFO2O50KqwFYeeuo4PsamUbFPx6bAdLbk9RA==",
|
||||
"dependencies": {
|
||||
"isomorphic-fetch": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
@@ -6169,53 +6160,6 @@
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/isomorphic-fetch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
|
||||
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1",
|
||||
"whatwg-fetch": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-fetch/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/isomorphic-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/isomorphic-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
|
||||
@@ -11235,11 +11179,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.19",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz",
|
||||
"integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw=="
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"@picocss/pico": "^1.5.9",
|
||||
"@primer/octicons-react": "^18.3.0",
|
||||
"buffer": "^6.0.3",
|
||||
"claude-ai": "^1.2.2",
|
||||
"countries-list": "^2.6.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"diff": "^5.1.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { pushRecord } from './shared.mjs'
|
||||
import Claude from 'claude-ai'
|
||||
import Claude from '../clients/claude'
|
||||
import { Models } from '../../config/index.mjs'
|
||||
|
||||
/**
|
||||
|
||||
+999
@@ -0,0 +1,999 @@
|
||||
// https://github.com/Explosion-Scratch/claude-unofficial-api
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* The main Claude API client class.
|
||||
* @typedef Claude
|
||||
* @class
|
||||
* @classdesc Creates an instance of the Claude API client.
|
||||
*/
|
||||
export class Claude {
|
||||
/**
|
||||
* If the Claude client has initialized yet (call `init()` if you haven't and this is false)
|
||||
* @property {boolean}
|
||||
*/
|
||||
ready
|
||||
/**
|
||||
* A proxy function/string to connect via
|
||||
* @property {({endpoint: string, options: Object}) => {endpoint: string, options: Object} | string}
|
||||
*/
|
||||
proxy
|
||||
/**
|
||||
* A fetch function, defaults to globalThis.fetch
|
||||
* @property {Function}
|
||||
*/
|
||||
fetch
|
||||
/**
|
||||
* The session key string (from the cookie)
|
||||
* @property {string}
|
||||
*/
|
||||
sessionKey
|
||||
/**
|
||||
* A UUID string
|
||||
* @typedef UUID
|
||||
* @example "222aa20a-bc79-48d2-8f6d-c819a1b5eaed"
|
||||
*/
|
||||
/**
|
||||
* Create a new Claude API client instance.
|
||||
* @param {Object} options - Options
|
||||
* @param {string} options.sessionKey - Claude session key
|
||||
* @param {string|function} [options.proxy] - Proxy URL or proxy function
|
||||
* @param {function} [options.fetch] - Fetch function
|
||||
* @example
|
||||
* const claude = new Claude({
|
||||
* sessionKey: 'sk-ant-sid01-*****',
|
||||
* fetch: globalThis.fetch
|
||||
* })
|
||||
*
|
||||
* await claude.init();
|
||||
* claude.sendMessage('Hello world').then(console.log)
|
||||
*/
|
||||
constructor({ sessionKey, proxy, fetch }) {
|
||||
this.ready = false
|
||||
if (typeof proxy === 'string') {
|
||||
const HOST = proxy
|
||||
this.proxy = ({ endpoint, options }) => ({ endpoint: HOST + endpoint, options })
|
||||
} else if (typeof proxy === 'function') {
|
||||
this.proxy = proxy
|
||||
} else if (proxy) {
|
||||
console.log(
|
||||
'Proxy supported formats:\n\t({ endpoint /* endpoint (path) */, options /* fetch options */ }) => { endpoint /* full url */, options /* fetch options */ }',
|
||||
)
|
||||
console.log('Received proxy: ' + proxy)
|
||||
throw new Error('Proxy must be a string (host) or a function')
|
||||
}
|
||||
if (!this.proxy) {
|
||||
this.proxy = ({ endpoint, options }) => ({
|
||||
endpoint: 'https://claude.ai' + endpoint,
|
||||
options,
|
||||
})
|
||||
}
|
||||
if (!sessionKey) {
|
||||
throw new Error('Session key required')
|
||||
}
|
||||
if (!sessionKey.startsWith('sk-ant-sid01')) {
|
||||
throw new Error('Session key invalid: Must be in the format sk-ant-sid01-*****')
|
||||
}
|
||||
if (fetch) {
|
||||
this.fetch = fetch
|
||||
}
|
||||
this.sessionKey = sessionKey
|
||||
}
|
||||
/**
|
||||
* Get available Claude models.
|
||||
* @returns {string[]} Array of model names
|
||||
*/
|
||||
models() {
|
||||
return ['claude-2', 'claude-1.3', 'claude-instant', 'claude-instant-100k']
|
||||
}
|
||||
/**
|
||||
* Get total token count for a Claude model.
|
||||
* @param {string} [model] - Claude model name
|
||||
* @returns {number} Total token count
|
||||
*/
|
||||
totalTokens(model) {
|
||||
// TODO: Figure out if this is correct, the blog article said "We’ve expanded Claude’s context window from 9K to 100K tokens"
|
||||
const TOKENS = {
|
||||
'claude-2': 100_000,
|
||||
'claude-1.3': 9000,
|
||||
'claude-instant': 9000,
|
||||
'claude-instant-100k': 100_000,
|
||||
}
|
||||
return TOKENS[model || this.defaultModel()]
|
||||
}
|
||||
/**
|
||||
* Get the default Claude model.
|
||||
* @returns {string} Default model name
|
||||
*/
|
||||
defaultModel() {
|
||||
return this.models()[0]
|
||||
}
|
||||
/**
|
||||
* A partial or total completion for a message.
|
||||
* @typedef MessageStreamChunk
|
||||
* @property {String} completion The markdown text completion for this response
|
||||
* @property {String | null} stop_reason The reason for the response stop (if any)
|
||||
* @property {String} model The model used
|
||||
* @property {String} stop The string at which Claude stopped responding at, e.g. "\n\nHuman:"
|
||||
* @property {String} log_id A logging ID
|
||||
* @property {Object} messageLimit If you're within the message limit
|
||||
* @param {String} messageLimit.type The type of message limit ("within_limit")
|
||||
*/
|
||||
/**
|
||||
* Send a message to a new or existing conversation.
|
||||
* @param {string} message - Initial message
|
||||
* @param {SendMessageParams} [params] - Additional parameters
|
||||
* @param {string} [params.conversation] - Existing conversation ID
|
||||
* @param {boolean} [params.temporary=true] - Delete after getting response
|
||||
* @returns {Promise<MessageStreamChunk>} Result message
|
||||
*/
|
||||
async sendMessage(message, { conversation = null, temporary = true, ...params }) {
|
||||
if (!conversation) {
|
||||
let out
|
||||
let convo = await this.startConversation(message, {
|
||||
...params,
|
||||
done: (a) => {
|
||||
if (params.done) {
|
||||
params.done(a)
|
||||
}
|
||||
out = a
|
||||
},
|
||||
})
|
||||
if (temporary) {
|
||||
await convo.delete()
|
||||
}
|
||||
return out
|
||||
} else {
|
||||
return (await this.getConversation(conversation)).sendMessage(message, {
|
||||
...params,
|
||||
})
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Make an API request.
|
||||
* @param {string} endpoint - API endpoint
|
||||
* @param {Object} options - Request options
|
||||
* @returns {Promise<Response>} Fetch response
|
||||
* @example
|
||||
* await claude.request('/api/organizations').then(r => r.json())
|
||||
*/
|
||||
request(endpoint, options) {
|
||||
// Can't figure out a way to test this so I'm just assuming it works
|
||||
if (!(this.fetch || globalThis.fetch)) {
|
||||
throw new Error(
|
||||
`No fetch available in your environment. Use node-18 or later, a modern browser, or add the following code to your project:\n\nimport "isomorphic-fetch";\nconst claude = new Claude({fetch: fetch, sessionKey: "sk-ant-sid01-*****"});`,
|
||||
)
|
||||
}
|
||||
if (!this.proxy) {
|
||||
this.proxy = ({ endpoint, options }) => ({
|
||||
endpoint: 'https://claude.ai' + endpoint,
|
||||
options,
|
||||
})
|
||||
}
|
||||
if (typeof this.proxy === 'string') {
|
||||
const HOST = this.proxy
|
||||
this.proxy = ({ endpoint, options }) => ({ endpoint: HOST + endpoint, options })
|
||||
}
|
||||
const proxied = this.proxy({ endpoint, options })
|
||||
return (this.fetch || globalThis.fetch)(proxied.endpoint, proxied.options)
|
||||
}
|
||||
/**
|
||||
* Initialize the client.
|
||||
* @async
|
||||
* @returns {Promise<void>} Void
|
||||
*/
|
||||
async init() {
|
||||
const organizations = await this.getOrganizations()
|
||||
if (organizations.error) {
|
||||
throw new Error(JSON.stringify(organizations, null, 2))
|
||||
}
|
||||
this.organizationId = organizations[0].uuid
|
||||
this.recent_conversations = await this.getConversations()
|
||||
this.ready = true
|
||||
}
|
||||
/**
|
||||
* An organization
|
||||
* @typedef Organization
|
||||
* @property {String} join_token A token
|
||||
* @property {String} name The organization name
|
||||
* @property {String} uuid The organization UUID
|
||||
* @property {String} created_at The organization creation date
|
||||
* @property {String} updated_at The organization update date
|
||||
* @property {String[]} capabilities What the organization can do
|
||||
* @property {Object} settings The organization's settings
|
||||
* @property {Array} active_flags Organization's flags (none that I've found)
|
||||
*/
|
||||
/**
|
||||
* Get the organizations list.
|
||||
* @async
|
||||
* @returns {Promise<Organization[]>} A list of organizations
|
||||
* @example
|
||||
* await claude.getOrganizations().then(organizations => {
|
||||
* console.log('Users organization name is:', organizations[0].name)
|
||||
* })
|
||||
*/
|
||||
async getOrganizations() {
|
||||
const response = await this.request('/api/organizations', {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: `sessionKey=${this.sessionKey}`,
|
||||
},
|
||||
})
|
||||
return await response.json().catch(errorHandle('getOrganizations'))
|
||||
}
|
||||
/**
|
||||
* Delete all conversations
|
||||
* @async
|
||||
* @returns {Promise<Response[]>} An array of responses for the DELETE requests
|
||||
* @example
|
||||
* await claude.clearConversations();
|
||||
* console.assert(await claude.getConversations().length === 0);
|
||||
*/
|
||||
async clearConversations() {
|
||||
const convos = await this.getConversations()
|
||||
return Promise.all(convos.map((i) => i.delete()))
|
||||
}
|
||||
/**
|
||||
* @callback doneCallback
|
||||
* @param {MessageStreamChunk} a The completed response
|
||||
*/
|
||||
/**
|
||||
* @callback progressCallback
|
||||
* @param {MessageStreamChunk} a The response in progress
|
||||
*/
|
||||
/**
|
||||
* Start a new conversation
|
||||
* @param {String} message The message to send to start the conversation
|
||||
* @param {SendMessageParams} [params={}] Message params passed to Conversation.sendMessage
|
||||
* @returns {Promise<Conversation>}
|
||||
* @async
|
||||
* @example
|
||||
* const conversation = await claude.startConversation("Hello! How are you?")
|
||||
* console.log(await conversation.getInfo());
|
||||
*/
|
||||
async startConversation(message, params = {}) {
|
||||
if (!this.ready) {
|
||||
await this.init()
|
||||
}
|
||||
const {
|
||||
uuid: convoID,
|
||||
name,
|
||||
summary,
|
||||
created_at,
|
||||
updated_at,
|
||||
} = await this.request(`/api/organizations/${this.organizationId}/chat_conversations`, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: `sessionKey=${this.sessionKey}`,
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: '',
|
||||
uuid: uuid(),
|
||||
}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.catch(errorHandle('startConversation create'))
|
||||
const convo = new Conversation(this, {
|
||||
conversationId: convoID,
|
||||
name,
|
||||
summary,
|
||||
created_at,
|
||||
updated_at,
|
||||
})
|
||||
await convo.sendMessage(message, params)
|
||||
await this.request(`/api/generate_chat_title`, {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: `sessionKey=${this.sessionKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
organization_uuid: this.organizationId,
|
||||
conversation_uuid: convoID,
|
||||
message_content: message,
|
||||
recent_titles: this.recent_conversations.map((i) => i.name),
|
||||
}),
|
||||
method: 'POST',
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.catch(errorHandle('startConversation generate_chat_title'))
|
||||
return convo
|
||||
}
|
||||
/**
|
||||
* Get a conversation by its ID
|
||||
* @param {UUID} id The uuid of the conversation (Conversation.uuid or Conversation.conversationId)
|
||||
* @async
|
||||
* @returns {Conversation | null} The conversation
|
||||
* @example
|
||||
* const conversation = await claude.getConversation("222aa20a-bc79-48d2-8f6d-c819a1b5eaed");
|
||||
*/
|
||||
async getConversation(id) {
|
||||
if (id instanceof Conversation || id.conversationId) {
|
||||
return new Conversation(this, { conversationId: id.conversationId })
|
||||
}
|
||||
return new Conversation(this, { conversationId: id })
|
||||
}
|
||||
/**
|
||||
* Get all conversations
|
||||
* @async
|
||||
* @returns {Promise<Conversation[]>} A list of conversations
|
||||
* @example
|
||||
* console.log(`You have ${await claude.getConversations().length} conversations:`);
|
||||
*/
|
||||
async getConversations() {
|
||||
const response = await this.request(
|
||||
`/api/organizations/${this.organizationId}/chat_conversations`,
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: `sessionKey=${this.sessionKey}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
const json = await response.json()
|
||||
return json.map((convo) => new Conversation(this, { conversationId: convo.uuid, ...convo }))
|
||||
}
|
||||
/**
|
||||
* The response from uploading a file (an attachment)
|
||||
* @typedef Attachment
|
||||
* @property {String} file_name The file name
|
||||
* @property {String} file_type The file's mime type
|
||||
* @property {Number} file_size The file size in bytes
|
||||
* @property {String} extracted_content The contents of the file that were extracted
|
||||
* @property {Number | null} [totalPages] The total pages of the document
|
||||
*/
|
||||
/**
|
||||
* Extract the contents of a file
|
||||
* @param {File} file A JS File (like) object to upload.
|
||||
* @async
|
||||
* @returns {Promise<Attachment>}
|
||||
* @example
|
||||
* const file = await claude.uploadFile(
|
||||
* new File(["test"], "test.txt", { type: "text/plain" }
|
||||
* );
|
||||
* console.log(await claude.sendMessage("What's the contents of test.txt?", {
|
||||
* attachments: [file]
|
||||
* }))
|
||||
*/
|
||||
async uploadFile(file) {
|
||||
const { content, isText } = await readAsText(file)
|
||||
if (isText) {
|
||||
console.log(`Extracted ${content.length} characters from ${file.name}`)
|
||||
return {
|
||||
file_name: file.name,
|
||||
file_type: file.type,
|
||||
file_size: file.size,
|
||||
extracted_content: content,
|
||||
}
|
||||
}
|
||||
const fd = new FormData()
|
||||
fd.append('file', file, file.name)
|
||||
fd.append('orgUuid', this.organizationId)
|
||||
const response = await this.request('/api/convert_document', {
|
||||
headers: {
|
||||
cookie: `sessionKey=${this.sessionKey}`,
|
||||
},
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
})
|
||||
let json
|
||||
try {
|
||||
json = await response.json()
|
||||
} catch (e) {
|
||||
console.log("Couldn't parse JSON", response.status)
|
||||
throw new Error('Invalid response when uploading ' + file.name)
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
console.log('Status not 200')
|
||||
throw new Error('Invalid response when uploading ' + file.name)
|
||||
}
|
||||
if (!json.hasOwnProperty('extracted_content')) {
|
||||
console.log(json)
|
||||
throw new Error('Invalid response when uploading ' + file.name)
|
||||
}
|
||||
console.log(`Extracted ${json.extracted_content.length} characters from ${file.name}`)
|
||||
return json
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef SendMessageParams
|
||||
* @property {Boolean} [retry=false] Whether to retry the most recent message in the conversation instead of sending a new one
|
||||
* @property {String} [timezone="America/New_York"] The timezone
|
||||
* @property {Attachment[]} [attachments=[]] Attachments
|
||||
* @property {doneCallback} [done] Callback when done receiving the message response
|
||||
* @property {progressCallback} [progress] Callback on message response progress
|
||||
* @property {string} [model=claude.defaultModel()] The model to use
|
||||
*/
|
||||
/**
|
||||
* A Claude conversation instance.
|
||||
* @class
|
||||
* @typedef Conversation
|
||||
* @classdesc Represents an active Claude conversation.
|
||||
*/
|
||||
export class Conversation {
|
||||
/**
|
||||
* The conversation ID
|
||||
* @property {string}
|
||||
*/
|
||||
conversationId
|
||||
|
||||
/**
|
||||
* The conversation name
|
||||
* @property {string}
|
||||
*/
|
||||
name
|
||||
|
||||
/**
|
||||
* The conversation summary (usually empty)
|
||||
* @property {string}
|
||||
*/
|
||||
summary
|
||||
|
||||
/**
|
||||
* The conversation created at
|
||||
* @property {string}
|
||||
*/
|
||||
created_at
|
||||
|
||||
/**
|
||||
* The conversation updated at
|
||||
* @property {string}
|
||||
*/
|
||||
updated_at
|
||||
|
||||
/**
|
||||
* The Claude client
|
||||
* @property {Claude}
|
||||
*/
|
||||
claude
|
||||
|
||||
/**
|
||||
* The request function (from parent claude instance)
|
||||
* @property {(url: string, options: object) => Response}
|
||||
*/
|
||||
request
|
||||
|
||||
/**
|
||||
* The current model
|
||||
* @property {string}
|
||||
*/
|
||||
model
|
||||
|
||||
/**
|
||||
* If the Claude client has initialized yet (call `init()` if you haven't and this is false)
|
||||
* @property {boolean}
|
||||
*/
|
||||
ready
|
||||
|
||||
/**
|
||||
* A proxy function/string to connect via
|
||||
* @property {({endpoint: string, options: Object}) => {endpoint: string, options: Object} | string}
|
||||
*/
|
||||
proxy
|
||||
|
||||
/**
|
||||
* A fetch function, defaults to globalThis.fetch
|
||||
* @property {Function}
|
||||
*/
|
||||
fetch
|
||||
/**
|
||||
* Create a Conversation instance.
|
||||
* @param {Claude} claude - Claude client instance
|
||||
* @param {Object} options - Options
|
||||
* @param {String} options.conversationId - Conversation ID
|
||||
* @param {String} [options.name] - Conversation name
|
||||
* @param {String} [options.summary] - Conversation summary
|
||||
* @param {String} [options.created_at] - Conversation created at
|
||||
* @param {String} [options.updated_at] - Conversation updated at
|
||||
* @param {String} [options.model] - Claude model
|
||||
*/
|
||||
constructor(
|
||||
claude,
|
||||
{ model = 'default', conversationId, name = '', summary = '', created_at, updated_at },
|
||||
) {
|
||||
this.claude = claude
|
||||
this.conversationId = conversationId
|
||||
this.request = claude.request
|
||||
if (!this.claude) {
|
||||
throw new Error('Claude not initialized')
|
||||
}
|
||||
if (!this.claude.sessionKey) {
|
||||
throw new Error('Session key required')
|
||||
}
|
||||
if (!this.conversationId) {
|
||||
throw new Error('Conversation ID required, are you calling `await claude.init()`?')
|
||||
}
|
||||
if (model === 'default') {
|
||||
model = this.claude.defaultModel()
|
||||
}
|
||||
this.model = model || this.claude.defaultModel()
|
||||
Object.assign(this, {
|
||||
name,
|
||||
summary,
|
||||
created_at: created_at || new Date().toISOString(),
|
||||
updated_at: updated_at || new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Convert the conversation to a JSON object
|
||||
* @returns {Conversation} The serializable object
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
conversationId: this.conversationId,
|
||||
uuid: this.conversationId,
|
||||
name: this.name,
|
||||
summary: this.summary,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
model: this.model,
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Retry the last message in the conversation
|
||||
* @param {SendMessageParams} [params={}]
|
||||
* @returns {Promise<MessageStreamChunk>}
|
||||
*/
|
||||
async retry(params) {
|
||||
return this.sendMessage('', { ...params, retry: true })
|
||||
}
|
||||
/**
|
||||
* Send a message to this conversation
|
||||
* @param {String} message
|
||||
* @async
|
||||
* @param {SendMessageParams} params The parameters to send along with the message
|
||||
* @returns {Promise<MessageStreamChunk>}
|
||||
*/
|
||||
async sendMessage(
|
||||
message,
|
||||
{
|
||||
retry = false,
|
||||
timezone = 'America/New_York',
|
||||
attachments = [],
|
||||
model = 'default',
|
||||
done = () => {},
|
||||
progress = () => {},
|
||||
rawResponse = () => {},
|
||||
} = {},
|
||||
) {
|
||||
if (model === 'default') {
|
||||
model = this.claude.defaultModel()
|
||||
}
|
||||
const body = {
|
||||
prompt: message,
|
||||
attachments,
|
||||
timezone,
|
||||
}
|
||||
const response = await this.request(
|
||||
`/api/organizations/${this.claude.organizationId}/chat_conversations/${this.conversationId}/${
|
||||
retry ? 'retry_completion' : 'completion'
|
||||
}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'text/event-stream,text/event-stream',
|
||||
'content-type': 'application/json',
|
||||
cookie: `sessionKey=${this.claude.sessionKey}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
)
|
||||
let resolve
|
||||
let returnPromise = new Promise((r) => (resolve = r))
|
||||
let parsed
|
||||
readStream(response, (a, fullResponse) => {
|
||||
rawResponse(a, fullResponse)
|
||||
if (!a.toString().startsWith('data:')) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
parsed = JSON.parse(
|
||||
a
|
||||
.toString()
|
||||
.replace(/^data\:/, '')
|
||||
.split('\n\ndata:')[0]
|
||||
?.trim() || '{}',
|
||||
)
|
||||
} catch (e) {
|
||||
return
|
||||
}
|
||||
const PROGRESS_OBJECT = {
|
||||
...parsed,
|
||||
completion: fullResponse
|
||||
.split('\n\n')
|
||||
.filter((i) => i.startsWith('data:'))
|
||||
.map((i) => {
|
||||
try {
|
||||
return JSON.parse(
|
||||
i
|
||||
.toString()
|
||||
.replace(/^data\: */, '')
|
||||
.split('\n\ndata:')[0]
|
||||
?.trim() || '{}',
|
||||
)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
.map((i) => i.completion)
|
||||
.join(''),
|
||||
delta: parsed.completion,
|
||||
}
|
||||
progress(PROGRESS_OBJECT)
|
||||
if (parsed.stop_reason === 'stop_sequence') {
|
||||
done(PROGRESS_OBJECT)
|
||||
resolve(PROGRESS_OBJECT)
|
||||
}
|
||||
})
|
||||
return returnPromise
|
||||
}
|
||||
/**
|
||||
* Rename the current conversation
|
||||
* @async
|
||||
* @param {String} title The new title
|
||||
* @returns {Promise<Response>} A Response object
|
||||
*/
|
||||
async rename(title) {
|
||||
if (!title?.length) {
|
||||
throw new Error('Title required')
|
||||
}
|
||||
return await this.request('/api/rename_chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
cookie: `sessionKey=${this.claude.sessionKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
conversation_uuid: this.conversationId,
|
||||
organization_uuid: this.claude.organizationId,
|
||||
title,
|
||||
}),
|
||||
}).catch(errorHandle('Rename conversation ' + this.conversationId))
|
||||
}
|
||||
/**
|
||||
* Delete the conversation
|
||||
* @async
|
||||
* @returns Promise<Response>
|
||||
*/
|
||||
async delete() {
|
||||
return await this.request(
|
||||
`/api/organizations/${this.claude.organizationId}/chat_conversations/${this.conversationId}`,
|
||||
{
|
||||
headers: {
|
||||
cookie: `sessionKey=${this.claude.sessionKey}`,
|
||||
},
|
||||
method: 'DELETE',
|
||||
},
|
||||
).catch(errorHandle('Delete conversation ' + this.conversationId))
|
||||
}
|
||||
/**
|
||||
* @typedef Message
|
||||
* @property {UUID} uuid The message UUID
|
||||
* @property {String} text The message text
|
||||
* @property {String} created_at The message created at
|
||||
* @property {String} updated_at The message updated at
|
||||
* @property {String | null} edited_at When the message was last edited (no editing support via api/web client)
|
||||
* @property {Any | null} chat_feedback Feedback
|
||||
* @property {Attachment[]} attachments The attachments
|
||||
*/
|
||||
/**
|
||||
* @typedef ConversationInfo
|
||||
* @extends Conversation
|
||||
* @property {Message[]} chat_messages The messages in this conversation
|
||||
*/
|
||||
/**
|
||||
* Get information about this conversation
|
||||
* @returns {Promise<ConversationInfo>}
|
||||
*/
|
||||
async getInfo() {
|
||||
const response = await this.request(
|
||||
`/api/organizations/${this.claude.organizationId}/chat_conversations/${this.conversationId}`,
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
cookie: `sessionKey=${this.claude.sessionKey}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
return await response
|
||||
.json()
|
||||
.then(this.#formatMessages('chat_messages'))
|
||||
.catch(errorHandle('getInfo'))
|
||||
}
|
||||
/**
|
||||
* Get all the files from this conversation
|
||||
* @async
|
||||
* @returns {Promise<Attachment[]>}
|
||||
*/
|
||||
getFiles() {
|
||||
return this.getMessages()
|
||||
.then((r) => r.map((i) => i.attachments))
|
||||
.then((r) => r.flat())
|
||||
.catch(errorHandle('getFiles'))
|
||||
}
|
||||
/**
|
||||
* Get all messages in the conversation
|
||||
* @async
|
||||
* @returns {Promise<Message[]>}
|
||||
*/
|
||||
getMessages() {
|
||||
return this.getInfo()
|
||||
.then((a) => a.chat_messages)
|
||||
.catch(errorHandle('getMessages'))
|
||||
}
|
||||
/**
|
||||
* Internal method for converting a JSON response to contain Message objects
|
||||
* @param {String} message_key The message key in the object
|
||||
* @returns {Function}
|
||||
*/
|
||||
#formatMessages(message_key) {
|
||||
return (response) => {
|
||||
if (!response[message_key]) {
|
||||
return response
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
[message_key]: response[message_key].map(
|
||||
(i) => new Message({ claude: this.claude, conversation: this }, { ...i }),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a stream and returns the decoded data as a string.
|
||||
*
|
||||
* @param {Response} response - The response object containing the stream.
|
||||
* @param {function} progressCallback - A callback function to track the progress of reading the stream.
|
||||
* @return {Promise<string>} - A promise that resolves with the decoded data as a string.
|
||||
*/
|
||||
async function readStream(response, progressCallback) {
|
||||
const reader = response.body.getReader()
|
||||
let received = 0
|
||||
let chunks = []
|
||||
let loading = true
|
||||
while (loading) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
loading = false
|
||||
break
|
||||
}
|
||||
chunks.push(value)
|
||||
received += value?.length || 0
|
||||
|
||||
let full = new Uint8Array(received)
|
||||
let position = 0
|
||||
|
||||
for (let chunk of chunks) {
|
||||
full.set(chunk, position)
|
||||
position += chunk.length
|
||||
}
|
||||
|
||||
if (value) {
|
||||
progressCallback(
|
||||
new TextDecoder('utf-8').decode(value),
|
||||
new TextDecoder('utf-8').decode(full),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let body = new Uint8Array(received)
|
||||
let position = 0
|
||||
|
||||
for (let chunk of chunks) {
|
||||
body.set(chunk, position)
|
||||
position += chunk.length
|
||||
}
|
||||
|
||||
return new TextDecoder('utf-8').decode(body)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of a file as text.
|
||||
*
|
||||
* @param {File} file - The file object to read.
|
||||
* @return {Object} - An object containing the content of the file and a flag indicating if it is a text file.
|
||||
*/
|
||||
async function readAsText(file) {
|
||||
const buf = await file.arrayBuffer()
|
||||
// const allow = ['text', 'javascript', 'json', 'html', 'sh', 'xml', 'latex', 'ecmascript']
|
||||
const notText = ['doc', 'pdf', 'ppt', 'xls']
|
||||
return {
|
||||
content: new TextDecoder('utf-8').decode(buf),
|
||||
isText: !notText.find((i) => file.name.includes(i)),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A function that handles errors.
|
||||
*
|
||||
* @param {string} msg - The error message.
|
||||
* @return {function} - A function that logs the error message and exits the process.
|
||||
*/
|
||||
function errorHandle(msg) {
|
||||
return (e) => {
|
||||
console.error(`Error at: ${msg}`)
|
||||
console.error(e)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random UUID.
|
||||
*
|
||||
* @return {UUID} A randomly generated UUID.
|
||||
*/
|
||||
function uuid() {
|
||||
var h = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
|
||||
var k = [
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'-',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'-',
|
||||
'4',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'-',
|
||||
'y',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'-',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
'x',
|
||||
]
|
||||
var u = '',
|
||||
i = 0,
|
||||
rb = (Math.random() * 0xffffffff) | 0
|
||||
while (i++ < 36) {
|
||||
var c = k[i - 1],
|
||||
r = rb & 0xf,
|
||||
v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||
u += c == '-' || c == '4' ? c : h[v]
|
||||
rb = i % 8 == 0 ? (Math.random() * 0xffffffff) | 0 : rb >> 4
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef JSONResponse
|
||||
* @property {'human' | 'assistant'} sender The sender
|
||||
* @property {string} text The text
|
||||
* @property {UUID} uuid msg uuid
|
||||
* @property {string} created_at The message created at
|
||||
* @property {string} updated_at The message updated at
|
||||
* @property {string} edited_at When the message was last edited (no editing support via api/web client)
|
||||
* @property {Attachment[]} attachments The attachments
|
||||
* @property {string} chat_feedback Feedback
|
||||
*/
|
||||
/**
|
||||
* Message class
|
||||
* @class
|
||||
* @classdesc A class representing a message in a Conversation
|
||||
* @property {Function} request The request function (inherited from claude instance)
|
||||
* @property {JSONResponse} json The JSON representation
|
||||
* @property {Claude} claude The claude instance
|
||||
* @property {Conversation} conversation The conversation this message belongs to
|
||||
* @property {UUID} uuid The message uuid
|
||||
*/
|
||||
export class Message {
|
||||
/**
|
||||
* Create a Message instance.
|
||||
* @param {Object} params - Params
|
||||
* @param {Conversation} params.conversation - Conversation instance
|
||||
* @param {Claude} params.claude - Claude instance
|
||||
* @param {Message} message - Message data
|
||||
*/
|
||||
constructor(
|
||||
{ conversation, claude },
|
||||
{ uuid, text, sender, index, updated_at, edited_at, chat_feedback, attachments },
|
||||
) {
|
||||
if (!claude) {
|
||||
throw new Error('Claude not initialized')
|
||||
}
|
||||
if (!conversation) {
|
||||
throw new Error('Conversation not initialized')
|
||||
}
|
||||
Object.assign(this, { conversation, claude })
|
||||
this.request = claude.request
|
||||
this.json = { uuid, text, sender, index, updated_at, edited_at, chat_feedback, attachments }
|
||||
Object.assign(this, this.json)
|
||||
}
|
||||
/**
|
||||
* Convert this message to a JSON representation
|
||||
* Necessary to prevent circular JSON errors
|
||||
* @returns {Message}
|
||||
*/
|
||||
toJSON() {
|
||||
return this.json
|
||||
}
|
||||
/**
|
||||
* Returns the value of the "created_at" property as a Date object.
|
||||
*
|
||||
* @return {Date} The value of the "created_at" property as a Date object.
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(this.json.created_at)
|
||||
}
|
||||
/**
|
||||
* Returns the value of the "updated_at" property as a Date object.
|
||||
*
|
||||
* @return {Date} The value of the "updated_at" property as a Date object.
|
||||
*/
|
||||
get updatedAt() {
|
||||
return new Date(this.json.updated_at)
|
||||
}
|
||||
/**
|
||||
* Returns the value of the "edited_at" property as a Date object.
|
||||
*
|
||||
* @return {Date} The value of the "edited_at" property as a Date object.
|
||||
*/
|
||||
get editedAt() {
|
||||
return new Date(this.json.edited_at)
|
||||
}
|
||||
/**
|
||||
* Get if message is from the assistant.
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isBot() {
|
||||
return this.sender === 'assistant'
|
||||
}
|
||||
/**
|
||||
* @typedef MessageFeedback
|
||||
* @property {UUID} uuid - Message UUID
|
||||
* @property {"flag/bug" | "flag/harmful" | "flag/other"} type - Feedback type
|
||||
* @property {String | null} reason - Feedback reason (details box)
|
||||
* @property {String} created_at - Feedback creation date
|
||||
* @property {String} updated_at - Feedback update date
|
||||
*/
|
||||
/**
|
||||
* Send feedback on the message.
|
||||
* @param {string} type - Feedback type
|
||||
* @param {string} [reason] - Feedback reason
|
||||
* @returns {Promise<MessageFeedback>} Response
|
||||
*/
|
||||
async sendFeedback(type, reason = '') {
|
||||
const FEEDBACK_TYPES = ['flag/bug', 'flag/harmful', 'flag/other']
|
||||
if (!FEEDBACK_TYPES.includes(type)) {
|
||||
throw new Error('Invalid feedback type, must be one of: ' + FEEDBACK_TYPES.join(', '))
|
||||
}
|
||||
return await this.request(
|
||||
`/api/organizations/${this.claude.organizationId}/chat_conversations/${this.conversation.conversationId}/chat_messages/${this.uuid}/chat_feedback`,
|
||||
{
|
||||
headers: {
|
||||
cookie: `sessionKey=${this.claude.sessionKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type,
|
||||
reason,
|
||||
}),
|
||||
method: 'POST',
|
||||
},
|
||||
).catch(errorHandle('Send feedback'))
|
||||
}
|
||||
}
|
||||
|
||||
export default Claude
|
||||
/* eslint-enable */
|
||||
Reference in New Issue
Block a user