Initial commit with the current state
This commit is contained in:
commit
1f0901d04d
|
@ -0,0 +1,183 @@
|
|||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
\*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
\*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
\*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
\*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
.cache/
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.\*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
|
||||
data
|
||||
vosk-*
|
||||
.wwebjs*
|
||||
apiUsage*
|
||||
*.db
|
||||
*.db-*
|
|
@ -0,0 +1,65 @@
|
|||
# anya
|
||||
|
||||
cheaper jarvis
|
||||
|
||||
Current Abilities:
|
||||
|
||||
- Multi user support.
|
||||
- Support for discord for user interaction and whatsapp for events.
|
||||
- Support for voice input through on_voice_message event.
|
||||
- Support for external events to trigger anya to execute any given instruction.
|
||||
- Support a schedule to trigger anya to execute any given instruction.
|
||||
- Can store memories for certain tasks.
|
||||
|
||||
Current Tools & Managers:
|
||||
|
||||
- Calculator: Can perform basic arithmetic operations.
|
||||
- Get time: Can tell the current time.
|
||||
- Calendar Manager (Uses CALDAV):
|
||||
- Can manage a user's calendar. (not yet configurable per user).
|
||||
- Cat Images: Can fetch random cat images.
|
||||
- Chat search: Can search for a chat message in a convo.
|
||||
- Communications Manager
|
||||
- Send Email: Can send an email to a user.
|
||||
- Send Message: Can send a message to a user. (supported platforms: discord, whatsapp)
|
||||
- Docker Container Shell: Can execute shell commands in an isolated docker container.
|
||||
- Events Manager
|
||||
- CRUD on Events: Setup events that can be listened to. (webhook based, need a one time manual setup for each event).
|
||||
- CRUD on Event Listeners: Setup event listeners that can call anya with a given instruction. once that event is triggered.
|
||||
- Files Tools (Currently disabled by default)
|
||||
- CRUD on a single s3/minio bucket.
|
||||
- Goole Search (Currently disabled): Can search google for a given query.
|
||||
- Home Assistant Manager:
|
||||
- Can update Services: Can run services to control devices on a home assistant instance.
|
||||
- LinkWarden Manager:
|
||||
- CRUD on Links: Manage links on a linkwarden instance.
|
||||
- Meme Generator: Can generate memes.
|
||||
- Memory Manager:
|
||||
- CRUD on Memories: Manage memories for anya and other managers.
|
||||
- Notes Manager:
|
||||
- CRUD on Notes: Manage notes with a defined template using webdav to perform crud on respective markdown notes files.
|
||||
- Periods Tools:
|
||||
- Period Tracking tools: can track cycles for a user.
|
||||
- Mood tracker per cycle: Can save mood events in an ongoing cycle.
|
||||
- Search: Can search through for events since the beginning of all tracking.
|
||||
- Reminder Manager (Uses CALDAV):
|
||||
- CRUD on Reminders: Manage reminders for a user.
|
||||
- Scraper (currently disabled): Can scrape a given website for a given query.
|
||||
- Youtube Tools:
|
||||
- Summerization: Can summerize a youtube video.
|
||||
- Searching in Video: Can search for a query in a youtube video.
|
||||
- Download: Can download a youtube video.
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.0.11. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
|
@ -0,0 +1,230 @@
|
|||
import { Message } from "../interfaces/message";
|
||||
import { format } from "date-fns";
|
||||
import { OpenAI } from "openai";
|
||||
import { getNotesSystemPrompt } from "../tools/notes";
|
||||
import { getReminderSystemPrompt } from "../tools/reminders";
|
||||
import { getCalendarSystemPrompt } from "../tools/calender";
|
||||
import { return_current_events } from "../tools/events";
|
||||
import { memory_manager_guide } from "../tools/memory-manager";
|
||||
|
||||
export async function buildSystemPrompts(
|
||||
context_message: Message
|
||||
): Promise<OpenAI.ChatCompletionMessageParam[]> {
|
||||
const userRoles = context_message.getUserRoles();
|
||||
const model = "gpt-4o-mini";
|
||||
|
||||
const general_tools_notes: OpenAI.ChatCompletionSystemMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `**Tool Notes:**
|
||||
|
||||
1. For scraping direct download links from non-YouTube sites in \`code_interpreter\`, include these dependencies:
|
||||
|
||||
\`\`\`
|
||||
[packages]
|
||||
aiohttp = "*"
|
||||
python-socketio = "~=5.0"
|
||||
yt-dlp = "*"
|
||||
\`\`\`
|
||||
|
||||
2. Use \`actions_manager\` to schedule actions for the user, like sending a message at a specific time or after a duration.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const admin_system_messages: OpenAI.ChatCompletionSystemMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `Your name is **Anya**.
|
||||
You are an AI assistant helping **Raj** manage tasks (functionally JARVIS for Raj).
|
||||
|
||||
Users interact with you via text or transcribed voice messages.
|
||||
|
||||
Your current memories saved by Memory Manager:
|
||||
---
|
||||
${memory_manager_guide("self")}
|
||||
---
|
||||
|
||||
**Interaction Guidelines:**
|
||||
- **Focused Responses:** Address user queries directly; avoid unnecessary information.
|
||||
- **Brevity:** Keep responses concise and to the point.
|
||||
|
||||
When context is provided inside a JSON message, it indicates a reply to the mentioned context.
|
||||
|
||||
Always reply in plain text or markdown unless running a tool.
|
||||
Ensure responses do not exceed 1500 characters.
|
||||
`,
|
||||
},
|
||||
{
|
||||
role: "system",
|
||||
content: `Current model being used: ${model}`,
|
||||
},
|
||||
...general_tools_notes,
|
||||
{
|
||||
role: "system",
|
||||
content: `**Context for Casual Conversation:**
|
||||
- Users are in India.
|
||||
- Use 12-hour time format.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const events = return_current_events().map((event) => event.eventId);
|
||||
const creator_system_messages: OpenAI.ChatCompletionSystemMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `You have access to **tool managers**.
|
||||
|
||||
When using tool managers:
|
||||
|
||||
- They may return extensive data; filter or summarize necessary information to provide what the user requested.
|
||||
- Validate the manager's response to ensure it meets the user's needs. If not, refine your prompt and try again.
|
||||
- Ensure your prompts to managers are clear and concise for desired outputs.
|
||||
|
||||
**Important:**
|
||||
|
||||
- Managers often maintain state across multiple calls, allowing for follow-up questions or additional information.
|
||||
- Managers are specialized LLMs for specific tasks; they perform better with detailed prompts.
|
||||
- Provide managers with as much detail as possible, e.g., user details when messaging someone specific.
|
||||
`,
|
||||
},
|
||||
{
|
||||
role: "system",
|
||||
content: `# **events_manager**
|
||||
Use the event manager to listen to external events.
|
||||
|
||||
- Each event can have multiple listeners, and each listener can have multiple actions.
|
||||
- Use this manager when the user wants something to happen based on an event.
|
||||
|
||||
**Examples:**
|
||||
- When I get an email, format it.
|
||||
- When I get home, turn on my room lights.
|
||||
- Send me an email when I receive one from Pooja.
|
||||
- Remind me to drink water at work.
|
||||
- When I get a message on WhatsApp from Pooja, reply that I'm asleep.
|
||||
|
||||
You can send these request directly to the event manager, you can add any more details if needed as you have more context about the user and conversation.
|
||||
|
||||
**Available Events:**
|
||||
${JSON.stringify(events)}
|
||||
|
||||
# **actions_manager**
|
||||
Use the actions manager to execute actions at a specific time or after a duration.
|
||||
|
||||
- An action is a single instruction to execute at a specified time or after a duration.
|
||||
- Use this manager when the user wants something to happen at a specific time or after a duration.
|
||||
- When including tool names that maybe required for the action, ensure that you describe the tool's role in the action in detail.
|
||||
|
||||
**Examples:**
|
||||
- User: Send me a message at 6 PM.
|
||||
Action Instruction: Notify user with some text at 6 PM.
|
||||
Tool Names: none (no need to use any tool to notify the creator of the action)
|
||||
|
||||
- User: Turn my Fan off every morning.
|
||||
Action Instruction: Ask 'home_assistant_manager' to turn off the fan every morning.
|
||||
Tool Names: ["home_assistant_manager"]
|
||||
|
||||
- Every Evening, show me yesterday's gym stats.
|
||||
Action Instruction: Fetch yesterday's gym stats by asking 'notes_manager' and send it to the user every evening around 6:30pm.
|
||||
Tool Names: ["notes_manager"]
|
||||
|
||||
- Tomorrow morning ping pooja that its an important day.
|
||||
Action Instruction: Tomorrow morning 8am ask 'communication_manager' to send a message to Pooja that it's an important day.
|
||||
Tool Names: ["communication_manager"]
|
||||
|
||||
In both managers, use the \`communication_manager\` tool to send messages to other users on any platform.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const regular_system_messages: OpenAI.ChatCompletionSystemMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `Your name is **Anya**.
|
||||
You are an AI that helps people in a server.
|
||||
|
||||
Users interact with you via text or transcribed voice messages.
|
||||
|
||||
**Interaction Guidelines:**
|
||||
- **Focused Responses:** Address user queries directly; avoid unnecessary information.
|
||||
- **Brevity:** Keep responses concise and to the point.
|
||||
|
||||
When context is provided inside a JSON message, it indicates a reply to the mentioned context.
|
||||
|
||||
Always reply in plain text or markdown unless running a tool.
|
||||
Ensure responses do not exceed 1500 characters.
|
||||
`,
|
||||
},
|
||||
...general_tools_notes,
|
||||
{
|
||||
role: "system",
|
||||
content: `Current model being used: ${model}`,
|
||||
},
|
||||
{
|
||||
role: "system",
|
||||
content: `**Context for Casual Conversation:**
|
||||
- Users are in India.
|
||||
- Use 12-hour time format.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
const menstrual_tracker_system_messages: OpenAI.ChatCompletionSystemMessageParam[] =
|
||||
[
|
||||
{
|
||||
role: "system",
|
||||
content: `This is a private conversation between you and the user **${
|
||||
context_message.author.config?.name || context_message.author.username
|
||||
}**.
|
||||
|
||||
Your task is to help them track and manage their menstrual cycle.
|
||||
|
||||
- Answer their queries and provide necessary information.
|
||||
- Point out any irregularities and suggest possible causes, but **DO NOT DIAGNOSE**.
|
||||
|
||||
**Current Date:** ${format(new Date(), "yyyy-MM-dd HH:mm:ss")} IST
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
let final_system_messages: OpenAI.ChatCompletionMessageParam[] = [];
|
||||
|
||||
// Determine which system messages to include based on user roles
|
||||
if (userRoles.includes("admin")) {
|
||||
final_system_messages = final_system_messages.concat(admin_system_messages);
|
||||
} else {
|
||||
final_system_messages = final_system_messages.concat(
|
||||
regular_system_messages
|
||||
);
|
||||
}
|
||||
|
||||
if (userRoles.includes("menstrualUser")) {
|
||||
final_system_messages = final_system_messages.concat(
|
||||
menstrual_tracker_system_messages
|
||||
);
|
||||
}
|
||||
|
||||
if (userRoles.includes("creator")) {
|
||||
final_system_messages = final_system_messages.concat(
|
||||
creator_system_messages
|
||||
);
|
||||
}
|
||||
|
||||
const memory_prompt: OpenAI.ChatCompletionSystemMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `**Note on Routing Memories:**
|
||||
|
||||
Make sure to route memories to the appropriate managers by requesting the respective managers to 'remember' the memory. Here are some guidelines:
|
||||
- All managers can save memories. Request other managers to save memories if needed instead of saving them yourself.
|
||||
- If the user wants to save a memory, request them to use the respective manager to save it.
|
||||
- If no other manager is appropriate, you can save the memory yourself.
|
||||
- Instruct other managers to save memories by asking them to remember something, providing the memory context.
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
final_system_messages = final_system_messages.concat(memory_prompt);
|
||||
|
||||
return final_system_messages;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import fs from "fs";
|
||||
import { z } from "zod";
|
||||
import path from "path";
|
||||
|
||||
export const dataDir = path.join(process.env.ANYA_DIR || "./");
|
||||
export const pathInDataDir = (filename: string) => path.join(dataDir, filename);
|
||||
|
||||
interface PlatformIdentity {
|
||||
platform: "discord" | "whatsapp" | "email" | "events";
|
||||
id: string; // Platform-specific user ID
|
||||
}
|
||||
|
||||
export interface UserConfig {
|
||||
name: string;
|
||||
identities: PlatformIdentity[];
|
||||
relatives?: {
|
||||
related_as: string[];
|
||||
user: UserConfig;
|
||||
}[];
|
||||
roles: string[]; // Roles assigned to the user
|
||||
}
|
||||
|
||||
// Define Zod schemas for validation
|
||||
const PlatformIdentitySchema = z.object({
|
||||
platform: z.enum(["discord", "whatsapp", "email", "events"]),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
const UserConfigSchema: z.ZodType<UserConfig> = z.lazy(() =>
|
||||
z.object({
|
||||
name: z.string(),
|
||||
identities: z.array(PlatformIdentitySchema),
|
||||
relatives: z
|
||||
.array(
|
||||
z.object({
|
||||
related_as: z.array(z.string()),
|
||||
user: UserConfigSchema, // recursive schema for relatives
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
roles: z.array(z.string()),
|
||||
})
|
||||
);
|
||||
|
||||
// Schema for the full configuration file
|
||||
const ConfigSchema = z.object({
|
||||
users: z.array(UserConfigSchema),
|
||||
rolePermissions: z.record(z.string(), z.array(z.string())),
|
||||
});
|
||||
|
||||
// Load user configuration data from file
|
||||
const userConfigPath = pathInDataDir("user-config.json");
|
||||
const rawData = fs.readFileSync(userConfigPath, "utf-8");
|
||||
const parsedData = JSON.parse(rawData);
|
||||
|
||||
// Validate the parsed JSON using the Zod schema
|
||||
const configData = ConfigSchema.parse(parsedData);
|
||||
|
||||
// Export the validated data
|
||||
export const { users: userConfigs, rolePermissions } = configData;
|
|
@ -0,0 +1,440 @@
|
|||
import { PlatformAdapter } from "../interfaces/platform-adapter";
|
||||
import { Message, SentMessage } from "../interfaces/message";
|
||||
import { getTools, zodFunction } from "../tools";
|
||||
import OpenAI from "openai";
|
||||
import { createHash } from "crypto";
|
||||
import { format } from "date-fns";
|
||||
import { saveApiUsage } from "../usage";
|
||||
import { buildSystemPrompts } from "../assistant/system-prompts";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { ask, get_transcription } from "../tools/ask";
|
||||
import { z } from "zod";
|
||||
import { send_sys_log } from "../interfaces/log";
|
||||
|
||||
interface MessageQueueEntry {
|
||||
abortController: AbortController;
|
||||
runningTools: boolean;
|
||||
}
|
||||
|
||||
export class MessageProcessor {
|
||||
private openai: OpenAI;
|
||||
private model: string = "gpt-4o-mini";
|
||||
private messageQueue: Map<string, MessageQueueEntry> = new Map();
|
||||
private toolsCallMap: Map<string, OpenAI.Chat.ChatCompletionMessageParam[]> =
|
||||
new Map();
|
||||
private channelIdHashMap: Map<string, string[]> = new Map();
|
||||
private sentMessage: SentMessage | null = null;
|
||||
|
||||
constructor(private adapter: PlatformAdapter) {
|
||||
this.openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY!,
|
||||
});
|
||||
}
|
||||
|
||||
public async processMessage(message: Message): Promise<void> {
|
||||
const userId = message.author.id;
|
||||
const channelId = message.channelId || userId; // Use message.id if channelId is not available
|
||||
|
||||
// Check if the message is a stop message
|
||||
if (["stop", "reset"].includes(message.content.toLowerCase())) {
|
||||
message.platform !== "whatsapp" &&
|
||||
(await message.send({
|
||||
content: "---setting this point as the start---",
|
||||
}));
|
||||
// Clear maps
|
||||
const hashes = this.channelIdHashMap.get(channelId) ?? [];
|
||||
hashes.forEach((hash) => {
|
||||
this.toolsCallMap.delete(hash);
|
||||
});
|
||||
this.channelIdHashMap.set(channelId, []);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.messageQueue.has(channelId)) {
|
||||
const queueEntry = this.messageQueue.get(channelId)!;
|
||||
if (!queueEntry.runningTools) {
|
||||
// Abort previous processing
|
||||
queueEntry.abortController.abort();
|
||||
this.messageQueue.delete(channelId);
|
||||
} else {
|
||||
// If tools are running, do not abort and return
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare OpenAI request
|
||||
const abortController = new AbortController();
|
||||
this.messageQueue.set(channelId, {
|
||||
abortController,
|
||||
runningTools: false,
|
||||
});
|
||||
|
||||
// Handle timeout
|
||||
setTimeout(async () => {
|
||||
const queueEntry = this.messageQueue.get(channelId);
|
||||
if (queueEntry && !queueEntry.runningTools) {
|
||||
abortController.abort();
|
||||
this.messageQueue.delete(channelId);
|
||||
await message.send({ content: "Timed out." });
|
||||
}
|
||||
}, 600000); // 10 minutes
|
||||
|
||||
try {
|
||||
// Indicate typing
|
||||
message.platformAdapter.config.indicators.typing &&
|
||||
(await message.sendTyping());
|
||||
|
||||
// Fetch message history
|
||||
const history = await this.adapter.fetchMessages(channelId, {
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
// Send 'thinking...' indicator
|
||||
if (message.platformAdapter.config.indicators.processing)
|
||||
this.sentMessage = await message.send({ content: "thinking..." });
|
||||
|
||||
// Check for stop message in history
|
||||
let stopIndex = -1;
|
||||
for (let i = 0; i < history.length; i++) {
|
||||
if (
|
||||
history[i].content === "---setting this point as the start---" ||
|
||||
history[i].content.replaceAll("!", "").trim() === "stop"
|
||||
) {
|
||||
stopIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const effectiveHistory =
|
||||
stopIndex !== -1 ? history.slice(0, stopIndex) : history;
|
||||
|
||||
// Construct AI messages
|
||||
const aiMessages = await this.constructAIMessages(
|
||||
effectiveHistory,
|
||||
message,
|
||||
channelId
|
||||
);
|
||||
|
||||
// Run tools and get AI response
|
||||
const response = await this.runAI(
|
||||
aiMessages as OpenAI.Chat.ChatCompletionMessage[],
|
||||
message.author.username,
|
||||
message,
|
||||
abortController,
|
||||
channelId
|
||||
);
|
||||
|
||||
// Send reply via adapter
|
||||
if (response && !response.includes("<NOREPLY>")) {
|
||||
const content = this.isJsonParseable(response);
|
||||
if (content && content.user_message) {
|
||||
await message.send({ content: content.user_message });
|
||||
} else {
|
||||
await message.send({ content: response });
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the thinking message
|
||||
if (this.sentMessage && this.sentMessage.deletable) {
|
||||
await this.sentMessage.delete();
|
||||
} else if (this.sentMessage) {
|
||||
// If not deletable, edit the message to indicate completion
|
||||
await this.sentMessage.edit({ content: "Response sent." });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing message:", error);
|
||||
await this.sentMessage?.delete();
|
||||
// await message.send({
|
||||
// content: "An error occurred while processing your message.",
|
||||
// });
|
||||
} finally {
|
||||
// Clean up
|
||||
this.messageQueue.delete(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
private async constructAIMessages(
|
||||
history: Message[],
|
||||
message: Message,
|
||||
channelId: string
|
||||
): Promise<OpenAI.Chat.ChatCompletionMessageParam[]> {
|
||||
// Build system prompts based on user roles
|
||||
const systemMessages: OpenAI.Chat.ChatCompletionMessageParam[] =
|
||||
await buildSystemPrompts(message);
|
||||
|
||||
// Map history messages to AI messages
|
||||
const channelHashes = this.channelIdHashMap.get(channelId) || [];
|
||||
|
||||
const aiMessagesArrays = await Promise.all(
|
||||
history.reverse().map(async (msg) => {
|
||||
const role =
|
||||
msg.author.id === this.adapter.getBotId() ? "assistant" : "user";
|
||||
|
||||
// Process attachments
|
||||
const files = (msg.attachments || [])
|
||||
.filter((a) => !a.url.includes("voice-message.ogg"))
|
||||
.map((a) => a.url);
|
||||
|
||||
const embeds = (msg.embeds || [])
|
||||
.map((e) => JSON.stringify(e))
|
||||
.join("\n");
|
||||
|
||||
// Transcribe voice messages
|
||||
const voiceMessagesPromises = (msg.attachments || [])
|
||||
.filter(
|
||||
(a) => a.url.includes("voice-message.ogg") || a.type === "ptt"
|
||||
)
|
||||
.map(async (a) => {
|
||||
const data =
|
||||
msg.platform === "whatsapp" ? (a.data as string) : a.url;
|
||||
const binary = msg.platform === "whatsapp";
|
||||
const key = msg.platform === "whatsapp" ? msg.id : undefined;
|
||||
return {
|
||||
file: a.url,
|
||||
transcription: await get_transcription(data, binary, key),
|
||||
};
|
||||
});
|
||||
|
||||
const voiceMessages = await Promise.all(voiceMessagesPromises);
|
||||
|
||||
// Process context message if any
|
||||
let contextMessage = null;
|
||||
if (msg.threadId) {
|
||||
contextMessage = history.find((m) => m.id === msg.threadId);
|
||||
// If not found, attempt to fetch it
|
||||
if (!contextMessage) {
|
||||
contextMessage = await this.adapter.fetchMessageById(
|
||||
channelId,
|
||||
msg.threadId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const contextAsJson = JSON.stringify({
|
||||
embeds: embeds || undefined,
|
||||
files: files.length > 0 ? files : undefined,
|
||||
user_message: msg.content,
|
||||
user_voice_messages:
|
||||
voiceMessages.length > 0 ? voiceMessages : undefined,
|
||||
created_at: format(msg.timestamp, "yyyy-MM-dd HH:mm:ss") + " IST",
|
||||
context_message: contextMessage
|
||||
? {
|
||||
author: contextMessage.author.username,
|
||||
created_at:
|
||||
format(contextMessage.timestamp, "yyyy-MM-dd HH:mm:ss") +
|
||||
" IST",
|
||||
content: contextMessage.content,
|
||||
}
|
||||
: undefined,
|
||||
context_files:
|
||||
contextMessage?.attachments?.map((a) => a.url) || undefined,
|
||||
context_embeds:
|
||||
contextMessage?.embeds?.map((e) => JSON.stringify(e)).join("\n") ||
|
||||
undefined,
|
||||
});
|
||||
|
||||
// get main user from userConfig
|
||||
const user = this.adapter.getUserById(msg.author.id);
|
||||
|
||||
const aiMessage: OpenAI.Chat.ChatCompletionMessageParam = {
|
||||
role,
|
||||
content: contextAsJson,
|
||||
name:
|
||||
user?.name ||
|
||||
msg.author.username.replace(/\s+/g, "_").substring(0, 64),
|
||||
};
|
||||
|
||||
// Handle tool calls mapping if necessary
|
||||
const hash = this.generateHash(msg.content);
|
||||
const calls = this.toolsCallMap.get(hash);
|
||||
if (calls) {
|
||||
return [aiMessage, ...calls];
|
||||
} else {
|
||||
return [aiMessage];
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Flatten aiMessages (since it's an array of arrays)
|
||||
let aiMessages = aiMessagesArrays.flat();
|
||||
|
||||
// Collect hashes
|
||||
history.forEach((msg) => {
|
||||
const hash = this.generateHash(msg.content);
|
||||
channelHashes.push(hash);
|
||||
});
|
||||
|
||||
// Update the channelIdHashMap
|
||||
this.channelIdHashMap.set(channelId, channelHashes);
|
||||
|
||||
// If the conversation history is too long, summarize it
|
||||
if (aiMessages.length > 25) {
|
||||
aiMessages = await this.summarizeConversation(aiMessages);
|
||||
}
|
||||
|
||||
// Combine system messages and conversation messages
|
||||
return systemMessages.concat(aiMessages);
|
||||
}
|
||||
|
||||
private async summarizeConversation(
|
||||
messages: OpenAI.Chat.ChatCompletionMessageParam[]
|
||||
): Promise<OpenAI.Chat.ChatCompletionMessageParam[]> {
|
||||
// Split the messages if necessary
|
||||
const lastTen = messages.slice(-10);
|
||||
const firstTen = messages.slice(0, 10);
|
||||
|
||||
// Use the OpenAI API to generate the summary
|
||||
const summaryResponse = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `Summarize the below conversation into 2 sections:
|
||||
1. General info about the conversation
|
||||
2. Tools used in the conversation and their data in relation to the conversation.
|
||||
|
||||
Conversation:
|
||||
----
|
||||
${YAML.stringify(firstTen)}
|
||||
----
|
||||
|
||||
Notes:
|
||||
- Keep only important information and points, remove anything repetitive.
|
||||
- Keep tools information if they are relevant.
|
||||
- The summary is to give context about the conversation that was happening previously.
|
||||
`,
|
||||
});
|
||||
|
||||
const summaryContent = summaryResponse.choices[0].message.content;
|
||||
|
||||
// Create a new conversation history with the summary
|
||||
const summarizedConversation: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: `Previous messages summarized:
|
||||
${summaryContent}
|
||||
`,
|
||||
},
|
||||
...lastTen,
|
||||
];
|
||||
|
||||
return summarizedConversation;
|
||||
}
|
||||
|
||||
private async runAI(
|
||||
messages: OpenAI.Chat.ChatCompletionMessage[],
|
||||
username: string,
|
||||
message: Message,
|
||||
abortController: AbortController,
|
||||
channelId: string
|
||||
): Promise<string> {
|
||||
const tmp = this;
|
||||
|
||||
async function changeModel({ model }: { model: string }) {
|
||||
tmp.model = model;
|
||||
console.log("Model changed to", model);
|
||||
return { message: "Model changed to " + model };
|
||||
}
|
||||
|
||||
// Use OpenAI to get a response, include tools integration
|
||||
const tools = getTools(username, message, "self");
|
||||
|
||||
const toolCalls: OpenAI.Chat.ChatCompletionMessageParam[] = [];
|
||||
|
||||
console.log("Current Model", this.model);
|
||||
const runner = this.openai.beta.chat.completions
|
||||
.runTools(
|
||||
{
|
||||
model: this.model,
|
||||
temperature: 0.6,
|
||||
user: username,
|
||||
messages,
|
||||
stream: true,
|
||||
tools: [
|
||||
zodFunction({
|
||||
name: "changeModel",
|
||||
schema: z.object({
|
||||
model: z.string(z.enum(["gpt-4o-mini", "gpt-4o"])),
|
||||
}),
|
||||
function: changeModel,
|
||||
description: `Change the model at run time.
|
||||
Default Model is 'gpt-4o-mini'.
|
||||
Current Model: ${this.model}
|
||||
Switch to 'gpt-4o' before running any other tool.
|
||||
Try to switch back to 'gpt-4o-mini' after running tools.
|
||||
`,
|
||||
}),
|
||||
...tools,
|
||||
],
|
||||
},
|
||||
{ signal: abortController.signal }
|
||||
)
|
||||
.on("functionCall", async (fnc) => {
|
||||
console.log("Function call:", fnc);
|
||||
// Set runningTools to true
|
||||
send_sys_log(`calling function: ${fnc.name}, in channel ${channelId}`);
|
||||
const queueEntry = this.messageQueue.get(channelId);
|
||||
|
||||
if (queueEntry) {
|
||||
queueEntry.runningTools = true;
|
||||
}
|
||||
// Indicate running tools
|
||||
if (this.sentMessage) {
|
||||
await this.sentMessage.edit({ content: `Running ${fnc.name}...` });
|
||||
} else await message.send({ content: `Running ${fnc.name}...` });
|
||||
})
|
||||
.on("message", (m) => {
|
||||
if (
|
||||
m.role === "assistant" &&
|
||||
(m.function_call || (m as any).tool_calls?.length)
|
||||
) {
|
||||
toolCalls.push(m);
|
||||
}
|
||||
if (
|
||||
(m.role === "function" || m.role === "tool") &&
|
||||
((m as any).function_call || (m as any).tool_call_id)
|
||||
) {
|
||||
toolCalls.push(m);
|
||||
}
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("Error:", err);
|
||||
send_sys_log(`Error: ${err}, in channel ${channelId}`);
|
||||
if (this.sentMessage)
|
||||
this.sentMessage.edit({ content: "Error: " + JSON.stringify(err) });
|
||||
else message.send({ content: "Error: " + JSON.stringify(err) });
|
||||
})
|
||||
.on("abort", () => {
|
||||
send_sys_log(`Aborting in channel ${channelId}`);
|
||||
console.log("Aborted");
|
||||
})
|
||||
.on("totalUsage", (stat) => {
|
||||
send_sys_log(`Usage: ${JSON.stringify(stat)}, in channel ${channelId}`);
|
||||
saveApiUsage(
|
||||
format(new Date(), "yyyy-MM-dd"),
|
||||
this.model,
|
||||
stat.prompt_tokens,
|
||||
stat.completion_tokens
|
||||
);
|
||||
});
|
||||
|
||||
const finalContent = await runner.finalContent();
|
||||
|
||||
// Store tool calls in toolsCallMap
|
||||
const hash = this.generateHash(messages[messages.length - 1].content || "");
|
||||
this.toolsCallMap.set(hash, toolCalls);
|
||||
|
||||
return finalContent ?? "";
|
||||
}
|
||||
|
||||
private isJsonParseable(str: string) {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private generateHash(input: string): string {
|
||||
const hash = createHash("sha256");
|
||||
hash.update(input);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
GROQ_BASE_URL=https://api.groq.com/openai/v1
|
||||
|
||||
# OPENAI_API_KEY=example_openai_api_key
|
||||
|
||||
ANYSCALE_TOKEN=example_anyscale_token
|
||||
OPENAI_API_KEY=example_openai_api_key
|
||||
|
||||
GROQ_API_KEY=example_groq_api_key
|
||||
|
||||
DISCORD_BOT_TOKEN=example_discord_bot_token
|
||||
DISCORD_CLIENT_ID=1186288013186179153
|
||||
|
||||
# Serp api token
|
||||
SEARCH_API_KEY=example_search_api_key
|
||||
|
||||
NEXTCLOUD_USERNAME=anya
|
||||
NEXTCLOUD_PASSWORD=example_password
|
||||
NEXTCLOUD_URL=https://cloud.tokio.space
|
||||
|
||||
# points to link.raj.how instance
|
||||
LINKWARDEN_API_KEY=example_linkwarden_api_key
|
||||
|
||||
# points to git git.raj.how instance
|
||||
GITEA_TOKEN=example_gitea_token
|
||||
|
||||
NEXTCLOUD_USERNAME=raj
|
||||
NEXTCLOUD_PASSWORD=example_password
|
||||
|
||||
MINIO_ACCESS_KEY=example_minio_access_key
|
||||
MINIO_SECRET_KEY=example_minio_secret_key
|
||||
|
||||
RESEND_API_KEY=example_resend_api_key
|
||||
|
||||
HA_KEY=example_ha_key
|
||||
|
||||
EVENTS_PORT=6006
|
||||
|
||||
DISCORD_LOG_CHANNEL_ID=1177561269780363247
|
||||
|
||||
ANYA_DIR=./data
|
|
@ -0,0 +1,3 @@
|
|||
import { startInterfaces } from "./interfaces";
|
||||
|
||||
startInterfaces();
|
|
@ -0,0 +1,386 @@
|
|||
import { PlatformAdapter, FetchOptions } from "./platform-adapter";
|
||||
import {
|
||||
Message as StdMessage,
|
||||
SentMessage,
|
||||
User as StdUser,
|
||||
Attachment,
|
||||
User,
|
||||
} from "./message";
|
||||
import {
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
Message as DiscordMessage,
|
||||
TextChannel,
|
||||
Partials,
|
||||
ChannelType,
|
||||
ActivityType,
|
||||
} from "discord.js";
|
||||
import { UserConfig, userConfigs } from "../config";
|
||||
|
||||
export class DiscordAdapter implements PlatformAdapter {
|
||||
private client: Client;
|
||||
private botUserId: string = "";
|
||||
|
||||
public config = {
|
||||
indicators: {
|
||||
typing: true,
|
||||
processing: true,
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.client = new Client({
|
||||
intents: [
|
||||
GatewayIntentBits.Guilds,
|
||||
GatewayIntentBits.GuildMessages,
|
||||
GatewayIntentBits.MessageContent,
|
||||
GatewayIntentBits.DirectMessages,
|
||||
],
|
||||
partials: [Partials.Channel],
|
||||
});
|
||||
|
||||
this.client.on("ready", () => {
|
||||
console.log(`Logged in as ${this.client.user?.tag}!`);
|
||||
this.botUserId = this.client.user?.id || "";
|
||||
this.client.user?.setActivity("as Human", {
|
||||
type: Number(ActivityType.Playing),
|
||||
});
|
||||
});
|
||||
|
||||
this.client.login(process.env.DISCORD_BOT_TOKEN);
|
||||
}
|
||||
|
||||
public getUserById(userId: string): UserConfig | null {
|
||||
const userConfig = userConfigs.find((user) =>
|
||||
user.identities.some(
|
||||
(identity) => identity.platform === "discord" && identity.id === userId
|
||||
)
|
||||
);
|
||||
|
||||
if (!userConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
}
|
||||
|
||||
public onMessage(callback: (message: StdMessage) => void): void {
|
||||
this.client.on("messageCreate", async (discordMessage: DiscordMessage) => {
|
||||
if (discordMessage.author.bot) return;
|
||||
|
||||
if (discordMessage.channel.type !== ChannelType.DM) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if user does not exist in userConfigs dont reply
|
||||
const userConfig = this.getUserById(discordMessage.author.id);
|
||||
if (!userConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await this.convertMessage(discordMessage);
|
||||
callback(message);
|
||||
});
|
||||
}
|
||||
|
||||
public async sendMessage(channelId: string, content: string): Promise<void> {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (
|
||||
channel?.type !== ChannelType.GuildText &&
|
||||
channel?.type !== ChannelType.DM
|
||||
) {
|
||||
console.error("Invalid channel type", channel?.type, channelId);
|
||||
return;
|
||||
}
|
||||
await (channel as TextChannel).send(content);
|
||||
}
|
||||
public async fetchMessageById(
|
||||
channelId: string,
|
||||
messageId: string
|
||||
): Promise<StdMessage | null> {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (
|
||||
!channel ||
|
||||
(channel.type !== ChannelType.GuildText &&
|
||||
channel.type !== ChannelType.DM)
|
||||
) {
|
||||
throw new Error("Invalid channel type");
|
||||
}
|
||||
|
||||
try {
|
||||
const discordMessage = await (channel as TextChannel).messages.fetch(
|
||||
messageId
|
||||
);
|
||||
const stdMessage = await this.convertMessage(discordMessage);
|
||||
return stdMessage;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message by ID: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public async fetchMessages(
|
||||
channelId: string,
|
||||
options: FetchOptions
|
||||
): Promise<StdMessage[]> {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (
|
||||
!channel ||
|
||||
(channel.type !== ChannelType.GuildText &&
|
||||
channel.type !== ChannelType.DM)
|
||||
) {
|
||||
throw new Error("Invalid channel type");
|
||||
}
|
||||
|
||||
const messages = await (channel as TextChannel).messages.fetch({
|
||||
limit: options.limit || 10,
|
||||
});
|
||||
const stdMessages: StdMessage[] = [];
|
||||
|
||||
for (const msg of messages.values()) {
|
||||
const stdMsg = await this.convertMessage(msg);
|
||||
stdMessages.push(stdMsg);
|
||||
}
|
||||
|
||||
// Return messages in chronological order
|
||||
return stdMessages;
|
||||
}
|
||||
|
||||
public getBotId(): string {
|
||||
return this.botUserId;
|
||||
}
|
||||
|
||||
public async sendSystemLog(content: string) {
|
||||
if (process.env.DISCORD_LOG_CHANNEL_ID)
|
||||
return await this.sendMessage(
|
||||
process.env.DISCORD_LOG_CHANNEL_ID || "",
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
public async searchUser(query: string): Promise<User[]> {
|
||||
const users = this.client.users.cache;
|
||||
return users
|
||||
.filter((user) =>
|
||||
user.username.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
.map((user) => ({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
config: this.getUserById(user.id),
|
||||
}));
|
||||
}
|
||||
|
||||
// Method to create a Message interface for a user ID
|
||||
public async createMessageInterface(userId: string): Promise<StdMessage> {
|
||||
try {
|
||||
const user = await this.client.users.fetch(userId);
|
||||
|
||||
console.log("creating message interface for: ", userId, user.username);
|
||||
|
||||
const stdMessage: StdMessage = {
|
||||
platform: "discord",
|
||||
platformAdapter: this,
|
||||
id: userId,
|
||||
author: {
|
||||
id: userId,
|
||||
username: user.username,
|
||||
config: this.getUserById(userId),
|
||||
},
|
||||
content: "",
|
||||
timestamp: new Date(),
|
||||
channelId: "",
|
||||
source: null,
|
||||
threadId: undefined,
|
||||
isDirectMessage: async () => true,
|
||||
send: async (messageData) => {
|
||||
const sentMessage = await user.send(messageData);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
reply: async (messageData) => {
|
||||
const sentMessage = await user.send(messageData);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
getUserRoles: () => {
|
||||
const userConfig = userConfigs.find((userConfig) =>
|
||||
userConfig.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "discord" && identity.id === userId
|
||||
)
|
||||
);
|
||||
return userConfig ? userConfig.roles : ["user"];
|
||||
},
|
||||
sendDirectMessage: async (userId, messageData) => {
|
||||
const user = await this.client.users.fetch(userId);
|
||||
console.log("sending message to: ", userId);
|
||||
await user.send(messageData);
|
||||
},
|
||||
sendMessageToChannel: async (channelId, messageData) => {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (channel?.isTextBased()) {
|
||||
await (channel as TextChannel).send(messageData);
|
||||
}
|
||||
},
|
||||
fetchChannelMessages: async (limit: number) => {
|
||||
const messages = await user.dmChannel?.messages.fetch({ limit });
|
||||
return Promise.all(
|
||||
messages?.map((msg) => this.convertMessage(msg)) || []
|
||||
);
|
||||
},
|
||||
sendFile: async (fileUrl, fileName) => {
|
||||
await user.dmChannel?.send({
|
||||
files: [{ attachment: fileUrl, name: fileName }],
|
||||
});
|
||||
},
|
||||
sendTyping: async () => {
|
||||
await user.dmChannel?.sendTyping();
|
||||
},
|
||||
};
|
||||
|
||||
return stdMessage;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create message interface for Discord user ${userId}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// You may also need to expose this method so it can be accessed elsewhere
|
||||
public getMessageInterface = this.createMessageInterface;
|
||||
|
||||
private async convertMessage(
|
||||
discordMessage: DiscordMessage
|
||||
): Promise<StdMessage> {
|
||||
const stdUser: StdUser = {
|
||||
id: discordMessage.author.id,
|
||||
username: discordMessage.author.username,
|
||||
config: this.getUserById(discordMessage.author.id),
|
||||
};
|
||||
|
||||
const attachments: Attachment[] = discordMessage.attachments.map(
|
||||
(attachment) => ({
|
||||
url: attachment.url,
|
||||
contentType: attachment.contentType || undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const stdMessage: StdMessage = {
|
||||
id: discordMessage.id,
|
||||
content: discordMessage.content,
|
||||
platformAdapter: this,
|
||||
author: stdUser,
|
||||
timestamp: discordMessage.createdAt,
|
||||
channelId: discordMessage.channelId,
|
||||
threadId: discordMessage.reference?.messageId || undefined,
|
||||
source: discordMessage,
|
||||
platform: "discord",
|
||||
attachments,
|
||||
isDirectMessage: async () =>
|
||||
discordMessage.channel.type === ChannelType.DM,
|
||||
send: async (messageData) => {
|
||||
const sentMessage = await discordMessage.channel.send(messageData);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
reply: async (messageData) => {
|
||||
const sentMessage = await discordMessage.reply(messageData);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
getUserRoles: () => {
|
||||
const userConfig = userConfigs.find((user) =>
|
||||
user.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "discord" &&
|
||||
identity.id === discordMessage.author.id
|
||||
)
|
||||
);
|
||||
return userConfig ? userConfig.roles : ["user"];
|
||||
},
|
||||
sendDirectMessage: async (userId, messageData) => {
|
||||
const user = await this.client.users.fetch(userId);
|
||||
await user.send(messageData);
|
||||
},
|
||||
sendMessageToChannel: async (channelId, messageData) => {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (channel?.isTextBased()) {
|
||||
await (channel as TextChannel).send(messageData);
|
||||
}
|
||||
},
|
||||
fetchChannelMessages: async (limit: number) => {
|
||||
const messages = await discordMessage.channel.messages.fetch({ limit });
|
||||
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||||
},
|
||||
sendFile: async (fileUrl, fileName) => {
|
||||
await discordMessage.channel.send({
|
||||
files: [{ attachment: fileUrl, name: fileName }],
|
||||
});
|
||||
},
|
||||
sendTyping: async () => {
|
||||
await discordMessage.channel.sendTyping();
|
||||
},
|
||||
};
|
||||
|
||||
return stdMessage;
|
||||
}
|
||||
|
||||
private convertSentMessage(discordMessage: DiscordMessage): SentMessage {
|
||||
return {
|
||||
id: discordMessage.id,
|
||||
platformAdapter: this,
|
||||
content: discordMessage.content,
|
||||
author: {
|
||||
id: discordMessage.author.id,
|
||||
username: discordMessage.author.username,
|
||||
config: this.getUserById(discordMessage.author.id),
|
||||
},
|
||||
timestamp: discordMessage.createdAt,
|
||||
channelId: discordMessage.channelId,
|
||||
threadId: discordMessage.reference?.messageId || undefined,
|
||||
source: discordMessage,
|
||||
platform: "discord",
|
||||
deletable: discordMessage.deletable,
|
||||
delete: async () => {
|
||||
if (discordMessage.deletable) {
|
||||
await discordMessage.delete();
|
||||
}
|
||||
},
|
||||
edit: async (data) => {
|
||||
await discordMessage.edit(data);
|
||||
},
|
||||
getUserRoles: () => {
|
||||
// Since this is a message sent by the bot, return bot's roles or empty array
|
||||
return [];
|
||||
},
|
||||
isDirectMessage: async () =>
|
||||
discordMessage.channel.type === ChannelType.DM,
|
||||
sendDirectMessage: async (userId, messageData) => {
|
||||
const user = await this.client.users.fetch(userId);
|
||||
await user.send(messageData);
|
||||
},
|
||||
sendMessageToChannel: async (channelId, messageData) => {
|
||||
const channel = await this.client.channels.fetch(channelId);
|
||||
if (channel?.isTextBased()) {
|
||||
await (channel as TextChannel).send(messageData);
|
||||
}
|
||||
},
|
||||
fetchChannelMessages: async (limit: number) => {
|
||||
const messages = await discordMessage.channel.messages.fetch({ limit });
|
||||
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||||
},
|
||||
sendFile: async (fileUrl, fileName) => {
|
||||
await discordMessage.channel.send({
|
||||
files: [{ attachment: fileUrl, name: fileName }],
|
||||
});
|
||||
},
|
||||
sendTyping: async () => {
|
||||
await discordMessage.channel.sendTyping();
|
||||
},
|
||||
reply: async (messageData) => {
|
||||
const sentMessage = await discordMessage.reply(messageData);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
send: async (messageData) => {
|
||||
const sentMessage = await discordMessage.channel.send(messageData);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,258 @@
|
|||
import { Elysia, t } from "elysia";
|
||||
import { userConfigs } from "../config";
|
||||
import { send_sys_log } from "./log";
|
||||
|
||||
// Define the type for the event callback
|
||||
type EventCallback = (
|
||||
payload: Record<string, string | number>
|
||||
) => void | Record<string, any> | Promise<void> | Promise<Record<string, any>>;
|
||||
|
||||
/**
|
||||
* EventManager handles registration and emission of events based on event IDs.
|
||||
*/
|
||||
class EventManager {
|
||||
private listeners: Map<string, Set<EventCallback>> = new Map();
|
||||
|
||||
/**
|
||||
* Registers a new listener for a specific event ID.
|
||||
* @param id - The event ID to listen for.
|
||||
* @param callback - The callback to invoke when the event is emitted.
|
||||
*/
|
||||
on(id: string, callback: EventCallback): void {
|
||||
if (!this.listeners.has(id)) {
|
||||
this.listeners.set(id, new Set());
|
||||
}
|
||||
this.listeners.get(id)!.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a specific listener for a given event ID.
|
||||
* @param id - The event ID.
|
||||
* @param callback - The callback to remove.
|
||||
*/
|
||||
off(id: string, callback: EventCallback): void {
|
||||
if (this.listeners.has(id)) {
|
||||
this.listeners.get(id)?.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event, triggering all registered listeners for the given event ID.
|
||||
* This method does not wait for listeners to complete and does not collect their responses.
|
||||
* @param id - The event ID to emit.
|
||||
* @param payload - The payload to pass to the listeners.
|
||||
*/
|
||||
emit(id: string, payload: Record<string, string | number>): void {
|
||||
const callbacks = this.listeners.get(id);
|
||||
if (callbacks) {
|
||||
callbacks.forEach((cb) => {
|
||||
try {
|
||||
cb(payload);
|
||||
} catch (error) {
|
||||
console.error(`Error in listener for event '${id}':`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event and waits for all listeners to complete.
|
||||
* Collects and returns the responses from the listeners.
|
||||
* @param id - The event ID to emit.
|
||||
* @param payload - The payload to pass to the listeners.
|
||||
* @returns An array of responses from the listeners.
|
||||
*/
|
||||
async emitWithResponse(
|
||||
id: string,
|
||||
payload: Record<string, string | number>
|
||||
): Promise<any[]> {
|
||||
const callbacks = this.listeners.get(id);
|
||||
const responses: any[] = [];
|
||||
|
||||
if (callbacks) {
|
||||
// Execute all callbacks and collect their responses
|
||||
const promises = Array.from(callbacks).map(async (cb) => {
|
||||
try {
|
||||
const result = cb(payload);
|
||||
if (result instanceof Promise) {
|
||||
return await result;
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error in listener for event '${id}':`, error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
// Filter out undefined or null responses
|
||||
results.forEach((res) => {
|
||||
if (res !== undefined && res !== null) {
|
||||
responses.push(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return responses;
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate the EventManager
|
||||
const eventManager = new EventManager();
|
||||
|
||||
// Create the Elysia server
|
||||
export const events = new Elysia()
|
||||
.get("/", () => "Anya\nExternal event listener running")
|
||||
.get(
|
||||
"/events/:id",
|
||||
async ({ params: { id }, query, headers }) => {
|
||||
const wait = query.wait;
|
||||
delete query.wait;
|
||||
|
||||
if (id === "ping") {
|
||||
console.log("Event received", query);
|
||||
send_sys_log(`Ping event received: ${JSON.stringify(query)}`);
|
||||
|
||||
if (wait) {
|
||||
const responses = await eventManager.emitWithResponse(
|
||||
"ping",
|
||||
query as Record<string, string | number>
|
||||
);
|
||||
return { response: "pong", listeners: responses };
|
||||
} else {
|
||||
eventManager.emit("ping", query as Record<string, string | number>);
|
||||
return "pong";
|
||||
}
|
||||
}
|
||||
|
||||
console.log("get hook", id);
|
||||
console.log("Event received", query);
|
||||
|
||||
if (!headers.token) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
const [username, password] = headers.token.split(":");
|
||||
const user = userConfigs.find((config) => config.name === username);
|
||||
|
||||
if (!user) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
const found = user.identities.find(
|
||||
(identity) => identity.platform === "events" && identity.id === password
|
||||
);
|
||||
|
||||
// console.log("found", found);
|
||||
if (!found) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
send_sys_log(`Event (${id}) received: ${JSON.stringify(query)}`);
|
||||
|
||||
if (wait) {
|
||||
const responses = await eventManager.emitWithResponse(
|
||||
id,
|
||||
query as Record<string, string | number>
|
||||
);
|
||||
return { response: "Event received", listeners: responses };
|
||||
} else {
|
||||
eventManager.emit(id, query as Record<string, string | number>);
|
||||
return "Event received";
|
||||
}
|
||||
},
|
||||
{
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
query: t.Object({
|
||||
wait: t.Optional(t.Boolean()),
|
||||
}),
|
||||
}
|
||||
)
|
||||
.post(
|
||||
"/events/:id",
|
||||
async ({ params: { id }, body, headers, query }) => {
|
||||
const wait = query.wait;
|
||||
|
||||
console.log("post hook", id);
|
||||
|
||||
// console.log("Event received", body);
|
||||
// Handle ArrayBuffer body
|
||||
if (body instanceof ArrayBuffer) {
|
||||
const textbody = new TextDecoder().decode(body as ArrayBuffer);
|
||||
try {
|
||||
body = JSON.parse(textbody);
|
||||
} catch (e) {
|
||||
body = textbody;
|
||||
}
|
||||
}
|
||||
// console.log("Event received", body);
|
||||
|
||||
if (id === "ping") {
|
||||
send_sys_log(`Ping event received: ${JSON.stringify(body)}`);
|
||||
if (wait) {
|
||||
const responses = await eventManager.emitWithResponse(
|
||||
"ping",
|
||||
body as Record<string, string | number>
|
||||
);
|
||||
return { response: "pong", listeners: responses };
|
||||
} else {
|
||||
eventManager.emit("ping", body as Record<string, string | number>);
|
||||
return "pong";
|
||||
}
|
||||
}
|
||||
|
||||
if (!headers.token) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
const [username, password] = headers.token.split(":");
|
||||
const user = userConfigs.find((config) => config.name === username);
|
||||
|
||||
if (!user) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
const found = user.identities.find(
|
||||
(identity) => identity.platform === "events" && identity.id === password
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
return { error: "Unauthorized" };
|
||||
}
|
||||
|
||||
send_sys_log(`Event (${id}) received: ${JSON.stringify(body)}`);
|
||||
|
||||
if (wait) {
|
||||
const responses = await eventManager.emitWithResponse(
|
||||
id,
|
||||
body as Record<string, string | number>
|
||||
);
|
||||
return { responses: responses };
|
||||
} else {
|
||||
eventManager.emit(id, body as Record<string, string | number>);
|
||||
return "Event received";
|
||||
}
|
||||
},
|
||||
{
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
body: t.Any(),
|
||||
query: t.Object({
|
||||
wait: t.Optional(t.Boolean()),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
// Function to start the server
|
||||
export function startEventsServer() {
|
||||
const port = parseInt(process.env.EVENTS_PORT || "7004", 10);
|
||||
events.listen(port, () => {
|
||||
console.log(`Events Server is running on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Export the eventManager to allow other modules to register listeners
|
||||
export { eventManager };
|
|
@ -0,0 +1,42 @@
|
|||
import { MessageProcessor } from "../core/message-processor";
|
||||
import { DiscordAdapter } from "./discord";
|
||||
import { startEventsServer } from "./events";
|
||||
import { Message } from "./message";
|
||||
import { WhatsAppAdapter } from "./whatsapp";
|
||||
|
||||
// Initialize Discord Adapter and Processor
|
||||
export const discordAdapter = new DiscordAdapter();
|
||||
|
||||
const discordProcessor = new MessageProcessor(discordAdapter);
|
||||
|
||||
// Initialize WhatsApp Adapter and Processor
|
||||
export const whatsappAdapter = new WhatsAppAdapter();
|
||||
const whatsappProcessor = new MessageProcessor(whatsappAdapter);
|
||||
|
||||
export function startInterfaces() {
|
||||
discordAdapter.onMessage(async (message) => {
|
||||
await discordProcessor.processMessage(message);
|
||||
});
|
||||
whatsappAdapter.onMessage(async (message) => {
|
||||
await whatsappProcessor.processMessage(message);
|
||||
});
|
||||
startEventsServer();
|
||||
}
|
||||
|
||||
export async function getMessageInterface(identity: {
|
||||
platform: string;
|
||||
id: string;
|
||||
}): Promise<Message> {
|
||||
try {
|
||||
switch (identity.platform) {
|
||||
case "discord":
|
||||
return await discordAdapter.createMessageInterface(identity.id);
|
||||
case "whatsapp":
|
||||
return await whatsappAdapter.createMessageInterface(identity.id);
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${identity.platform}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`getMessageInterface error: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { discordAdapter } from ".";
|
||||
|
||||
export function send_sys_log(content: string) {
|
||||
return discordAdapter.sendSystemLog(content);
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import { UserConfig } from "../config";
|
||||
import { PlatformAdapter } from "./platform-adapter";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
config?: UserConfig | null;
|
||||
meta?: any;
|
||||
}
|
||||
|
||||
export interface SentMessage extends Message {
|
||||
deletable: boolean;
|
||||
delete: () => Promise<void>;
|
||||
edit: (data: any) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
url: string;
|
||||
contentType?: string;
|
||||
data?: Buffer | string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface Embed {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface MessageData {
|
||||
content?: string;
|
||||
embeds?: Embed[];
|
||||
file?:
|
||||
| {
|
||||
url: string;
|
||||
}
|
||||
| { path: string };
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
id: string;
|
||||
content: string;
|
||||
author: User;
|
||||
timestamp: Date;
|
||||
channelId: string;
|
||||
threadId?: string;
|
||||
attachments?: Attachment[];
|
||||
embeds?: Embed[];
|
||||
source: any; // Original message object (from Discord or WhatsApp)
|
||||
platform: "discord" | "whatsapp" | "other";
|
||||
reply: (data: MessageData) => Promise<SentMessage>;
|
||||
send: (data: MessageData) => Promise<SentMessage>;
|
||||
getUserRoles: () => string[];
|
||||
isDirectMessage: () => Promise<boolean>;
|
||||
sendDirectMessage: (
|
||||
userId: string,
|
||||
messageData: MessageData
|
||||
) => Promise<void>;
|
||||
sendMessageToChannel: (
|
||||
channelId: string,
|
||||
messageData: MessageData
|
||||
) => Promise<void>;
|
||||
sendFile: (fileUrl: string, fileName: string) => Promise<void>;
|
||||
fetchChannelMessages: (limit: number) => Promise<Message[]>;
|
||||
sendTyping: () => Promise<void>;
|
||||
platformAdapter: PlatformAdapter;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { UserConfig } from "../config";
|
||||
import { Message, User } from "./message";
|
||||
|
||||
export interface FetchOptions {
|
||||
limit?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
}
|
||||
|
||||
export interface PlatformAdapter {
|
||||
onMessage(callback: (message: Message) => void): void;
|
||||
sendMessage(channelId: string, content: string): Promise<void>;
|
||||
fetchMessages(channelId: string, options: FetchOptions): Promise<Message[]>;
|
||||
fetchMessageById(
|
||||
channelId: string,
|
||||
messageId: string
|
||||
): Promise<Message | null>;
|
||||
getBotId(): string; // For identifying bot's own messages
|
||||
getUserById(userId: string): UserConfig | null;
|
||||
sendSystemLog?(content: string): Promise<void>;
|
||||
searchUser(query: string): Promise<User[]>;
|
||||
config: {
|
||||
indicators: {
|
||||
typing: boolean;
|
||||
processing: boolean;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,546 @@
|
|||
import { PlatformAdapter, FetchOptions } from "./platform-adapter";
|
||||
import {
|
||||
Message as StdMessage,
|
||||
User as StdUser,
|
||||
SentMessage,
|
||||
Attachment,
|
||||
} from "./message";
|
||||
import {
|
||||
Client as WAClient,
|
||||
Message as WAMessage,
|
||||
LocalAuth,
|
||||
MessageMedia,
|
||||
} from "whatsapp-web.js";
|
||||
import { UserConfig, userConfigs } from "../config";
|
||||
import { eventManager } from "./events";
|
||||
import { return_current_listeners } from "../tools/events";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
// const allowedUsers = ["pooja", "raj"];
|
||||
const allowedUsers: string[] = [];
|
||||
|
||||
export class WhatsAppAdapter implements PlatformAdapter {
|
||||
private client: WAClient;
|
||||
private botUserId: string = "918884016724@c.us";
|
||||
|
||||
public config = {
|
||||
indicators: {
|
||||
typing: false,
|
||||
processing: false,
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.client = new WAClient({
|
||||
authStrategy: new LocalAuth(),
|
||||
});
|
||||
try {
|
||||
this.client.on("ready", () => {
|
||||
console.log("WhatsApp Client is ready!");
|
||||
});
|
||||
|
||||
this.client.initialize();
|
||||
} catch (error) {
|
||||
console.log(`Failed to initialize WhatsApp client: `, error);
|
||||
}
|
||||
}
|
||||
public getUserById(userId: string): UserConfig | null {
|
||||
const userConfig = userConfigs.find((user) =>
|
||||
user.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "whatsapp" && `${identity.id}@c.us` === userId
|
||||
)
|
||||
);
|
||||
|
||||
if (!userConfig) {
|
||||
// console.log(`User not found for WhatsApp ID: ${userId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return userConfig;
|
||||
}
|
||||
|
||||
public onMessage(callback: (message: StdMessage) => void): void {
|
||||
this.client.on("message_create", async (waMessage: WAMessage) => {
|
||||
// emit internal event only if text message and there is an active listener
|
||||
const listeners = return_current_listeners();
|
||||
if (
|
||||
typeof waMessage.body === "string" &&
|
||||
!waMessage.fromMe &&
|
||||
listeners.find((l) => l.eventId.includes("whatsapp"))
|
||||
) {
|
||||
const contact = await this.client.getContactById(waMessage.from);
|
||||
eventManager.emit("got_whatsapp_message", {
|
||||
sender_id: waMessage.from,
|
||||
sender_contact_name:
|
||||
contact.name || contact.shortName || contact.pushname || "NA",
|
||||
timestamp: waMessage.timestamp,
|
||||
content: waMessage.body,
|
||||
profile_image_url: await contact.getProfilePicUrl(),
|
||||
is_group_message: contact.isGroup.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
// user must exist in userConfigs
|
||||
const usr = this.getUserById(waMessage.from);
|
||||
if (!usr) {
|
||||
console.log(`Ignoring ID: ${waMessage.from}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// user must be in allowedUsers
|
||||
if (!allowedUsers.includes(usr.name)) {
|
||||
// console.log(`User not allowed: ${usr.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore messages sent by the bot
|
||||
if (waMessage.fromMe) return;
|
||||
const message = await this.convertMessage(waMessage);
|
||||
|
||||
callback(message);
|
||||
});
|
||||
}
|
||||
|
||||
public async sendMessage(channelId: string, content: string): Promise<void> {
|
||||
await this.client.sendMessage(channelId, content);
|
||||
}
|
||||
|
||||
public async fetchMessages(
|
||||
channelId: string,
|
||||
options: FetchOptions
|
||||
): Promise<StdMessage[]> {
|
||||
const chat = await this.client.getChatById(channelId);
|
||||
const messages = await chat.fetchMessages({ limit: options.limit || 10 });
|
||||
|
||||
const stdMessages: StdMessage[] = [];
|
||||
|
||||
for (const msg of messages) {
|
||||
const stdMsg = await this.convertMessage(msg);
|
||||
stdMessages.push(stdMsg);
|
||||
}
|
||||
|
||||
// Return messages in chronological order
|
||||
return stdMessages.reverse();
|
||||
}
|
||||
|
||||
public getBotId(): string {
|
||||
return this.botUserId;
|
||||
}
|
||||
|
||||
public async createMessageInterface(userId: string): Promise<StdMessage> {
|
||||
try {
|
||||
const contact = await this.client.getContactById(userId);
|
||||
const stdMessage: StdMessage = {
|
||||
platform: "whatsapp",
|
||||
platformAdapter: this,
|
||||
id: userId,
|
||||
author: {
|
||||
id: userId,
|
||||
username:
|
||||
contact.name || contact.shortName || contact.pushname || "NA",
|
||||
config: this.getUserById(userId),
|
||||
},
|
||||
content: "", // Placeholder content
|
||||
timestamp: new Date(), // Placeholder timestamp
|
||||
channelId: userId, // Assuming userId is the channelId
|
||||
source: null, // Placeholder source
|
||||
threadId: undefined, // Placeholder threadId
|
||||
isDirectMessage: async () => true,
|
||||
sendDirectMessage: async (recipientId, messageData) => {
|
||||
const tcontact = await this.client.getContactById(recipientId);
|
||||
const tchat = await tcontact.getChat();
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
|
||||
await tchat.sendMessage(messageData.content || "", { media });
|
||||
},
|
||||
sendMessageToChannel: async (channelId, messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
await this.client.sendMessage(channelId, messageData.content || "", {
|
||||
media,
|
||||
});
|
||||
},
|
||||
sendFile: async (fileUrl, fileName) => {
|
||||
const media = MessageMedia.fromFilePath(fileUrl);
|
||||
await this.client.sendMessage(userId, media, {
|
||||
caption: fileName,
|
||||
});
|
||||
},
|
||||
fetchChannelMessages: async (limit: number) => {
|
||||
const chat = await this.client.getChatById(userId);
|
||||
const messages = await chat.fetchMessages({ limit });
|
||||
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||||
},
|
||||
getUserRoles: () => {
|
||||
const userConfig = userConfigs.find((user) =>
|
||||
user.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "whatsapp" && identity.id === userId
|
||||
)
|
||||
);
|
||||
return userConfig ? userConfig.roles : ["user"];
|
||||
},
|
||||
send: async (messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
const sentMessage = await this.client.sendMessage(
|
||||
userId,
|
||||
messageData.content || "",
|
||||
{
|
||||
media,
|
||||
}
|
||||
);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
reply: async (messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
|
||||
const sentMessage = await this.client.sendMessage(
|
||||
userId,
|
||||
messageData.content || "",
|
||||
{
|
||||
media,
|
||||
}
|
||||
);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
sendTyping: async () => {
|
||||
// WhatsApp Web API does not support sending typing indicators directly
|
||||
// This method can be left as a no-op or you can implement a workaround if possible
|
||||
},
|
||||
};
|
||||
|
||||
return stdMessage;
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to create message interface for WhatsApp user ${userId}: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
async searchUser(query: string): Promise<StdUser[]> {
|
||||
try {
|
||||
const contacts = await this.client.getContacts();
|
||||
|
||||
const stdcontacts = await Promise.all(
|
||||
contacts
|
||||
.filter((c) => c.isMyContact)
|
||||
.map(async (contact) => {
|
||||
return {
|
||||
id: contact.id._serialized,
|
||||
username:
|
||||
contact.pushname || contact.name || contact.shortName || "NA",
|
||||
config: this.getUserById(contact.id._serialized),
|
||||
meta: {
|
||||
about: contact.getAbout(),
|
||||
verifiedName: contact.verifiedName,
|
||||
shortName: contact.shortName,
|
||||
pushname: contact.pushname,
|
||||
name: contact.name,
|
||||
profilePicUrl: await contact.getProfilePicUrl(),
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
console.log("Starting search");
|
||||
const fuse = new Fuse(stdcontacts, {
|
||||
keys: ["id", "username"],
|
||||
threshold: 0.3,
|
||||
});
|
||||
const results = fuse.search(query);
|
||||
console.log("search done", results.length);
|
||||
return results.map((result) => result.item);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to search for WhatsApp contacts: ${error}`);
|
||||
}
|
||||
}
|
||||
// Expose this method so it can be accessed elsewhere
|
||||
public getMessageInterface = this.createMessageInterface;
|
||||
|
||||
private async convertMessage(waMessage: WAMessage): Promise<StdMessage> {
|
||||
const contact = await waMessage.getContact();
|
||||
|
||||
const stdUser: StdUser = {
|
||||
id: contact.id._serialized,
|
||||
username: contact.name || contact.shortName || contact.pushname || "NA",
|
||||
config: this.getUserById(contact.id._serialized),
|
||||
};
|
||||
|
||||
// Convert attachments
|
||||
let attachments: Attachment[] = [];
|
||||
if (waMessage.hasMedia) {
|
||||
const media = await waMessage.downloadMedia();
|
||||
|
||||
attachments.push({
|
||||
url: "", // WhatsApp does not provide a direct URL to the media
|
||||
data: media.data,
|
||||
contentType: media.mimetype,
|
||||
type: waMessage.type,
|
||||
});
|
||||
}
|
||||
|
||||
const stdMessage: StdMessage = {
|
||||
id: waMessage.id._serialized,
|
||||
content: waMessage.body,
|
||||
platformAdapter: this,
|
||||
author: stdUser,
|
||||
timestamp: new Date(waMessage.timestamp * 1000),
|
||||
channelId: waMessage.from,
|
||||
threadId: waMessage.hasQuotedMsg
|
||||
? (await waMessage.getQuotedMessage()).id._serialized
|
||||
: undefined,
|
||||
source: waMessage,
|
||||
platform: "whatsapp",
|
||||
isDirectMessage: async () => {
|
||||
const chat = await this.client.getChatById(waMessage.from);
|
||||
return !chat.isGroup; // Returns true if not a group chat
|
||||
},
|
||||
sendDirectMessage: async (recipientId, messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
await this.client.sendMessage(recipientId, messageData.content || "", {
|
||||
media,
|
||||
});
|
||||
},
|
||||
sendMessageToChannel: async (channelId, messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
await this.client.sendMessage(channelId, messageData.content || "", {
|
||||
media,
|
||||
});
|
||||
},
|
||||
sendFile: async (fileUrl, fileName) => {
|
||||
const media = MessageMedia.fromFilePath(fileUrl);
|
||||
await this.client.sendMessage(waMessage.from, media, {
|
||||
caption: fileName,
|
||||
});
|
||||
},
|
||||
fetchChannelMessages: async (limit: number) => {
|
||||
const chat = await this.client.getChatById(waMessage.from);
|
||||
const messages = await chat.fetchMessages({ limit });
|
||||
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||||
},
|
||||
getUserRoles: () => {
|
||||
const userConfig = userConfigs.find((user) =>
|
||||
user.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "whatsapp" &&
|
||||
identity.id === contact.id._serialized
|
||||
)
|
||||
);
|
||||
return userConfig ? userConfig.roles : ["user"];
|
||||
},
|
||||
send: async (messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
const sentMessage = await this.client.sendMessage(
|
||||
waMessage.from,
|
||||
messageData.content || "",
|
||||
{
|
||||
media,
|
||||
}
|
||||
);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
reply: async (messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
const sentMessage = await this.client.sendMessage(
|
||||
waMessage.from,
|
||||
messageData.content || "",
|
||||
{
|
||||
media,
|
||||
}
|
||||
);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
sendTyping: async () => {
|
||||
// WhatsApp Web API does not support sending typing indicators directly
|
||||
// You may leave this as a no-op
|
||||
},
|
||||
};
|
||||
|
||||
return stdMessage;
|
||||
}
|
||||
|
||||
public async fetchMessageById(
|
||||
channelId: string,
|
||||
messageId: string
|
||||
): Promise<StdMessage | null> {
|
||||
try {
|
||||
const waMessage = await this.client.getMessageById(messageId);
|
||||
if (waMessage) {
|
||||
const stdMessage = await this.convertMessage(waMessage);
|
||||
return stdMessage;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch message by ID: ${error}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async convertSentMessage(
|
||||
sentWAMessage: WAMessage
|
||||
): Promise<SentMessage> {
|
||||
const contact = await sentWAMessage.getContact();
|
||||
|
||||
return {
|
||||
id: sentWAMessage.id._serialized,
|
||||
platformAdapter: this,
|
||||
content: sentWAMessage.body,
|
||||
author: {
|
||||
id: contact.id._serialized,
|
||||
username:
|
||||
contact.name ||
|
||||
contact.shortName ||
|
||||
contact.pushname ||
|
||||
contact.number,
|
||||
config: this.getUserById(contact.id._serialized),
|
||||
},
|
||||
timestamp: new Date(sentWAMessage.timestamp * 1000),
|
||||
channelId: sentWAMessage.from,
|
||||
threadId: sentWAMessage.hasQuotedMsg
|
||||
? (await sentWAMessage.getQuotedMessage()).id._serialized
|
||||
: undefined,
|
||||
source: sentWAMessage,
|
||||
platform: "whatsapp",
|
||||
deletable: true,
|
||||
delete: async () => {
|
||||
await sentWAMessage.delete();
|
||||
},
|
||||
edit: async (messageData) => {
|
||||
sentWAMessage.edit(messageData.content || "");
|
||||
},
|
||||
reply: async (messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
const replyMessage = await sentWAMessage.reply(
|
||||
messageData.content || "",
|
||||
sentWAMessage.id._serialized,
|
||||
{ media }
|
||||
);
|
||||
return this.convertSentMessage(replyMessage);
|
||||
},
|
||||
send: async (messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
const sentMessage = await this.client.sendMessage(
|
||||
sentWAMessage.from,
|
||||
messageData.content || "",
|
||||
{
|
||||
media,
|
||||
}
|
||||
);
|
||||
return this.convertSentMessage(sentMessage);
|
||||
},
|
||||
getUserRoles: () => {
|
||||
const userConfig = userConfigs.find((user) =>
|
||||
user.identities.some(
|
||||
(identity) =>
|
||||
identity.platform === "whatsapp" &&
|
||||
identity.id === contact.id._serialized
|
||||
)
|
||||
);
|
||||
return userConfig ? userConfig.roles : ["user"];
|
||||
},
|
||||
isDirectMessage: async () => {
|
||||
const chat = await this.client.getChatById(sentWAMessage.from);
|
||||
return !chat.isGroup; // Returns true if not a group chat
|
||||
},
|
||||
sendDirectMessage: async (recipientId, messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
await this.client.sendMessage(recipientId, messageData.content || "", {
|
||||
media,
|
||||
});
|
||||
},
|
||||
sendMessageToChannel: async (channelId, messageData) => {
|
||||
let media;
|
||||
if (messageData.file && "url" in messageData.file) {
|
||||
media = await MessageMedia.fromUrl(messageData.file.url);
|
||||
}
|
||||
if (messageData.file && "path" in messageData.file) {
|
||||
media = MessageMedia.fromFilePath(messageData.file.path);
|
||||
}
|
||||
await this.client.sendMessage(channelId, messageData.content || "", {
|
||||
media,
|
||||
});
|
||||
},
|
||||
sendFile: async (fileUrl, fileName) => {
|
||||
const media = MessageMedia.fromFilePath(fileUrl);
|
||||
await this.client.sendMessage(sentWAMessage.from, media, {
|
||||
caption: fileName,
|
||||
});
|
||||
},
|
||||
fetchChannelMessages: async (limit: number) => {
|
||||
const chat = await this.client.getChatById(sentWAMessage.from);
|
||||
const messages = await chat.fetchMessages({ limit });
|
||||
return Promise.all(messages.map((msg) => this.convertMessage(msg)));
|
||||
},
|
||||
sendTyping: async () => {
|
||||
// WhatsApp Web API does not support sending typing indicators directly
|
||||
// You may leave this as a no-op
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "anya",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "^2.1.24",
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "bunx @puppeteer/browsers install chrome@115.0.5790.98 --path $HOME/.cache/puppeteer"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.15",
|
||||
"@nextcloud/files": "^3.8.0",
|
||||
"@solyarisoftware/voskjs": "^1.2.8",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"axios": "^1.7.3",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"date-fns": "^3.6.0",
|
||||
"discord.js": "^14.14.1",
|
||||
"elysia": "^1.1.17",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"form-data": "^4.0.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"fuzzysort": "^3.0.2",
|
||||
"i": "^0.3.7",
|
||||
"langchain": "^0.0.212",
|
||||
"mathjs": "^12.2.1",
|
||||
"meta-fetcher": "^3.1.1",
|
||||
"minio": "^8.0.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"nextcloud-node-client": "^1.8.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"npm": "^10.2.5",
|
||||
"openai": "^4.67.1",
|
||||
"pyodide": "^0.24.1",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"quickchart-js": "^3.1.3",
|
||||
"resend": "^4.0.0",
|
||||
"serpapi": "^2.0.0",
|
||||
"turndown": "^7.2.0",
|
||||
"whatsapp-web.js": "^1.26.0",
|
||||
"whisper-node": "^1.1.1",
|
||||
"xml2js": "^0.6.2",
|
||||
"youtube-transcript": "^1.2.1",
|
||||
"youtubei.js": "^5.8.0",
|
||||
"ytdl-core": "^4.11.5",
|
||||
"zod": "^3.22.4",
|
||||
"zod-to-json-schema": "^3.23.0",
|
||||
"zx": "^7.2.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,714 @@
|
|||
// actions.ts
|
||||
import YAML from "yaml";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { eventManager } from "../interfaces/events";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { discordAdapter } from "../interfaces";
|
||||
import { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
|
||||
import { getTools, zodFunction } from ".";
|
||||
import { ask } from "./ask";
|
||||
import Fuse from "fuse.js";
|
||||
import cron from "node-cron";
|
||||
import { pathInDataDir, userConfigs } from "../config";
|
||||
import { get_event_listeners } from "./events";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
|
||||
// Paths to the JSON files
|
||||
const ACTIONS_FILE_PATH = pathInDataDir("actions.json");
|
||||
|
||||
// Define schema for creating an action
|
||||
export const CreateActionParams = z.object({
|
||||
actionId: z
|
||||
.string()
|
||||
.describe(
|
||||
"The unique identifier for the action. Make this relevant to the action."
|
||||
),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, "description is required")
|
||||
.describe("Short description of the action."),
|
||||
schedule: z
|
||||
.object({
|
||||
type: z.enum(["delay", "cron"]).describe("Type of scheduling."),
|
||||
time: z.union([
|
||||
z.number().positive().int().describe("Delay in seconds."),
|
||||
z.string().describe("Cron expression."),
|
||||
]),
|
||||
})
|
||||
.describe("Scheduling details for the action."),
|
||||
instruction: z
|
||||
.string()
|
||||
.min(1, "instruction is required")
|
||||
.describe(
|
||||
"Detailed instructions on what to do when the action is executed."
|
||||
),
|
||||
tool_names: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
`Names of the tools required to execute the instruction of an action.
|
||||
Each of these should look something like "home_assistant_manager" or "calculator" and NOT "function:home_assistant_manager" or "function.calculator".`
|
||||
),
|
||||
notify: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"Wheater to notify the user when the action is executed or not with the action's output."
|
||||
),
|
||||
});
|
||||
|
||||
// Type for creating an action
|
||||
export type CreateActionParams = z.infer<typeof CreateActionParams>;
|
||||
|
||||
// Define schema for searching actions
|
||||
export const SearchActionsParams = z.object({
|
||||
userId: z.string().optional(),
|
||||
actionId: z.string().optional(),
|
||||
});
|
||||
|
||||
// Type for searching actions
|
||||
export type SearchActionsParams = z.infer<typeof SearchActionsParams>;
|
||||
|
||||
// Define schema for removing an action
|
||||
const RemoveActionParamsSchema = z.object({
|
||||
actionId: z.string().min(1, "actionId is required"),
|
||||
});
|
||||
|
||||
// Type for removing an action
|
||||
type RemoveActionParams = z.infer<typeof RemoveActionParamsSchema>;
|
||||
|
||||
// Define schema for updating an action
|
||||
export const UpdateActionParams = z
|
||||
.object({
|
||||
actionId: z.string().min(1, "actionId is required"),
|
||||
description: z.string().min(1, "description is required"),
|
||||
schedule: z
|
||||
.object({
|
||||
type: z.enum(["delay", "cron"]).describe("Type of scheduling."),
|
||||
time: z.union([
|
||||
z.number().positive().int().describe("Delay in seconds."),
|
||||
z.string().describe("Cron expression."),
|
||||
]),
|
||||
})
|
||||
.describe("Scheduling details for the action."),
|
||||
instruction: z
|
||||
.string()
|
||||
.min(1, "instruction is required")
|
||||
.describe(
|
||||
"Detailed instructions on what to do when the action is executed."
|
||||
)
|
||||
.optional(),
|
||||
template: z
|
||||
.string()
|
||||
.min(1, "template is required")
|
||||
.describe(
|
||||
"A string template to format the action payload. Use double curly braces to reference variables, e.g., {{variableName}}."
|
||||
)
|
||||
.optional(),
|
||||
tool_names: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Names of the tools required to execute the instruction when the action is executed."
|
||||
),
|
||||
notify: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
"Whether to notify the user when the action is executed or not with the action's output."
|
||||
),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasInstruction = !!data.instruction;
|
||||
const hasTemplate = !!data.template;
|
||||
return hasInstruction !== hasTemplate; // Either instruction or template must be present, but not both
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Either 'instruction' with 'tool_names' or 'template' must be provided, but not both.",
|
||||
}
|
||||
);
|
||||
|
||||
// Type for updating an action
|
||||
export type UpdateActionParams = z.infer<typeof UpdateActionParams>;
|
||||
|
||||
// Define the structure of an Action
|
||||
interface Action {
|
||||
actionId: string;
|
||||
description: string;
|
||||
userId: string;
|
||||
schedule: {
|
||||
type: "delay" | "cron";
|
||||
time: number | string;
|
||||
};
|
||||
instruction?: string;
|
||||
template?: string;
|
||||
tool_names?: string[];
|
||||
notify: boolean;
|
||||
created_at: string; // ISO string for serialization
|
||||
}
|
||||
|
||||
// In-memory storage for actions
|
||||
const actionsMap: Map<string, Action> = new Map();
|
||||
|
||||
// Helper function to load actions from the JSON file
|
||||
async function loadActionsFromFile() {
|
||||
try {
|
||||
const data = await fs.readFile(ACTIONS_FILE_PATH, "utf-8");
|
||||
const parsed = JSON.parse(data) as Action[];
|
||||
parsed.forEach((action) => {
|
||||
actionsMap.set(action.actionId, action);
|
||||
scheduleAction(action);
|
||||
});
|
||||
console.log(
|
||||
`✅ Loaded ${actionsMap.size} actions from ${ACTIONS_FILE_PATH}`
|
||||
);
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
// File does not exist, create an empty file
|
||||
await saveActionsToFile();
|
||||
console.log(`📄 Created new actions file at ${ACTIONS_FILE_PATH}`);
|
||||
} else {
|
||||
console.error(`❌ Failed to load actions from file: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to save actions to the JSON file
|
||||
async function saveActionsToFile() {
|
||||
const data = JSON.stringify(Array.from(actionsMap.values()), null, 2);
|
||||
await fs.writeFile(ACTIONS_FILE_PATH, data, "utf-8");
|
||||
}
|
||||
|
||||
// Function to schedule an action based on its schedule
|
||||
function scheduleAction(action: Action) {
|
||||
if (
|
||||
action.schedule.type === "delay" &&
|
||||
typeof action.schedule.time === "number"
|
||||
) {
|
||||
const createdAt = new Date(action.created_at).getTime();
|
||||
const currentTime = Date.now();
|
||||
const delayInMs = action.schedule.time * 1000;
|
||||
const elapsedTime = currentTime - createdAt;
|
||||
const remainingTime = delayInMs - elapsedTime;
|
||||
|
||||
if (remainingTime > 0) {
|
||||
setTimeout(async () => {
|
||||
await executeAction(action);
|
||||
// After execution, remove the action as it's a one-time delay
|
||||
actionsMap.delete(action.actionId);
|
||||
await saveActionsToFile();
|
||||
console.log(`🗑️ Removed action "${action.actionId}" after execution.`);
|
||||
}, remainingTime);
|
||||
console.log(
|
||||
`⏰ Scheduled action "${action.actionId}" to run in ${Math.round(
|
||||
remainingTime / 1000
|
||||
)} seconds.`
|
||||
);
|
||||
} else {
|
||||
// If the remaining time is less than or equal to zero, execute immediately
|
||||
executeAction(action).then(async () => {
|
||||
actionsMap.delete(action.actionId);
|
||||
await saveActionsToFile();
|
||||
console.log(
|
||||
`🗑️ Removed action "${action.actionId}" after immediate execution.`
|
||||
);
|
||||
});
|
||||
console.log(
|
||||
`⚡ Executed action "${action.actionId}" immediately as the delay has already passed.`
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
action.schedule.type === "cron" &&
|
||||
typeof action.schedule.time === "string"
|
||||
) {
|
||||
// Schedule the action using the cron expression
|
||||
cron.schedule(action.schedule.time, () => {
|
||||
executeAction(action);
|
||||
});
|
||||
|
||||
console.log(
|
||||
`🕒 Scheduled action "${action.actionId}" with cron expression "${action.schedule.time}".`
|
||||
);
|
||||
} else {
|
||||
console.error(`❌ Invalid schedule for action "${action.actionId}".`);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to execute an action
|
||||
async function executeAction(action: Action) {
|
||||
try {
|
||||
// Recreate the Message instance using discordAdapter
|
||||
const contextMessage: Message = await discordAdapter.createMessageInterface(
|
||||
action.userId
|
||||
);
|
||||
if (!contextMessage) {
|
||||
console.error(
|
||||
`❌ Unable to create Message interface for user "${action.userId}".`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.template) {
|
||||
// Handle static action with template
|
||||
const payload = {}; // Define how to obtain payload if needed
|
||||
const formattedMessage = renderTemplate(action.template, payload);
|
||||
await contextMessage.send({ content: formattedMessage });
|
||||
} else if (action.instruction && action.tool_names) {
|
||||
// Handle dynamic action with instruction and tools
|
||||
let tools = getTools(
|
||||
contextMessage.author.username,
|
||||
contextMessage
|
||||
).filter(
|
||||
(tool) =>
|
||||
tool.function.name && action.tool_names?.includes(tool.function.name)
|
||||
) as RunnableToolFunctionWithParse<any>[] | undefined;
|
||||
|
||||
tools = tools?.length ? tools : undefined;
|
||||
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `You are an Action Executor.
|
||||
|
||||
You are called to execute an action based on the provided instruction.
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
1. **Notifying the Current User:**
|
||||
- Any message you reply with will automatically be sent to the user as a notification.
|
||||
|
||||
**Example:**
|
||||
|
||||
- **Instruction:** "Tell Pooja happy birthday"
|
||||
- **Tool Names:** ["communication_manager"]
|
||||
- **Notify:** true
|
||||
- **Steps:**
|
||||
1. Ask \`communication_manager\` to wish Pooja a happy birthday to send a message to the recipient mentioned by the user.
|
||||
2. Reply to the current user with "I wished Pooja a happy birthday." to notify the user.
|
||||
|
||||
**Action Details:**
|
||||
|
||||
- **Action ID:** ${action.actionId}
|
||||
- **Description:** ${action.description}
|
||||
- **Instruction:** ${action.instruction}
|
||||
|
||||
Use the required tools/managers as needed.
|
||||
`,
|
||||
tools: tools?.length ? tools : undefined,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content ?? undefined;
|
||||
|
||||
// Send a message to the user indicating the action was executed
|
||||
await contextMessage.send({ content });
|
||||
} else {
|
||||
console.error(
|
||||
`❌ Action "${action.actionId}" has neither 'instruction' nor 'template' defined properly.`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error executing action "${action.actionId}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple template renderer that replaces {{key}} with corresponding values from payload.
|
||||
* @param template - The string template containing placeholders like {{key}}.
|
||||
* @param payload - The payload containing key-value pairs.
|
||||
* @returns The formatted string with placeholders replaced by payload values.
|
||||
*/
|
||||
function renderTemplate(
|
||||
template: string,
|
||||
payload: Record<string, string>
|
||||
): string {
|
||||
return template.replace(/{{\s*([^}]+)\s*}}/g, (_, key) => {
|
||||
return payload[key.trim()] || `{{${key.trim()}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action.
|
||||
* @param params - Parameters for creating the action.
|
||||
* @param contextMessage - The message context from which the action is created.
|
||||
* @returns A JSON object containing the action details and a success message, or an error.
|
||||
*/
|
||||
export async function create_action(
|
||||
params: CreateActionParams,
|
||||
contextMessage: Message
|
||||
): Promise<any> {
|
||||
const parsed = CreateActionParams.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.errors };
|
||||
}
|
||||
|
||||
let { actionId, description, schedule, instruction, tool_names } =
|
||||
parsed.data;
|
||||
|
||||
// Get the userId from contextMessage
|
||||
const userId: string = contextMessage.author.id;
|
||||
|
||||
if (actionsMap.has(actionId)) {
|
||||
return { error: `❌ Action with ID "${actionId}" already exists.` };
|
||||
}
|
||||
|
||||
const send_message_tools = tool_names?.filter(
|
||||
(t) => t.includes("send_message") && !t.startsWith("confirm_")
|
||||
);
|
||||
if (send_message_tools?.length) {
|
||||
return {
|
||||
confirmation: `You are using tools that sends a message to the user explicitly.
|
||||
Tool names that triggered this confirmation: [${send_message_tools.join(
|
||||
", "
|
||||
)}]
|
||||
Use these only to send a message to a different user or channel. Not sending this tool would by default send the message to the user anyway.
|
||||
To use this tool anyway like to send a message to a different user or channel, please re run create command and prefix the tool name with 'confirm_'`,
|
||||
};
|
||||
}
|
||||
|
||||
tool_names = tool_names?.map((t) =>
|
||||
t.startsWith("confirm_") ? t.replace("confirm_", "") : t
|
||||
);
|
||||
|
||||
const action: Action = {
|
||||
actionId,
|
||||
description,
|
||||
userId,
|
||||
schedule,
|
||||
instruction,
|
||||
tool_names,
|
||||
notify: params.notify ?? true,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
actionsMap.set(actionId, action);
|
||||
await saveActionsToFile();
|
||||
|
||||
// Schedule the action
|
||||
scheduleAction(action);
|
||||
|
||||
return {
|
||||
actionId,
|
||||
description,
|
||||
userId,
|
||||
schedule,
|
||||
instruction,
|
||||
tool_names,
|
||||
created_at: action.created_at,
|
||||
message: "✅ Action created and scheduled successfully.",
|
||||
};
|
||||
}
|
||||
|
||||
// 1. Define schema for getting actions
|
||||
export const GetActionsParams = z.object({});
|
||||
|
||||
export type GetActionsParams = z.infer<typeof GetActionsParams>;
|
||||
|
||||
// 2. Implement the get_actions function
|
||||
export async function get_actions(
|
||||
params: GetActionsParams,
|
||||
contextMessage: Message
|
||||
): Promise<any> {
|
||||
// Get the userId from contextMessage
|
||||
const userId: string = contextMessage.author.id;
|
||||
|
||||
// Get all actions created by this user
|
||||
const userActions = Array.from(actionsMap.values()).filter(
|
||||
(action) => action.userId === userId
|
||||
);
|
||||
|
||||
return {
|
||||
actions: userActions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an action by its actionId by fully deleting it.
|
||||
* @param params - Parameters containing the actionId.
|
||||
* @returns A JSON object confirming removal or an error.
|
||||
*/
|
||||
export async function remove_action(params: RemoveActionParams): Promise<any> {
|
||||
// Validate parameters using zod
|
||||
const parsed = RemoveActionParamsSchema.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.errors };
|
||||
}
|
||||
|
||||
const { actionId } = parsed.data;
|
||||
|
||||
const action = actionsMap.get(actionId);
|
||||
|
||||
if (!action) {
|
||||
return {
|
||||
error: `❌ Action with ID "${actionId}" not found.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Remove the action from the map
|
||||
actionsMap.delete(actionId);
|
||||
await saveActionsToFile();
|
||||
|
||||
// Note: In a real implementation, you'd also need to cancel the scheduled task.
|
||||
// This can be managed by keeping track of timers or using a scheduler that supports cancellation.
|
||||
|
||||
return {
|
||||
message: `✅ Action with ID "${actionId}" removed successfully.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the details of an action.
|
||||
* @param params - Parameters containing the actionId and fields to update.
|
||||
* @param contextMessage - The message context to identify the user.
|
||||
* @returns A JSON object confirming the update or an error.
|
||||
*/
|
||||
export async function update_action(
|
||||
params: UpdateActionParams,
|
||||
contextMessage: Message
|
||||
): Promise<any> {
|
||||
// Validate parameters using zod
|
||||
const parsed = UpdateActionParams.safeParse(params);
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.errors };
|
||||
}
|
||||
|
||||
const {
|
||||
actionId,
|
||||
description,
|
||||
schedule,
|
||||
instruction,
|
||||
template,
|
||||
tool_names,
|
||||
notify,
|
||||
} = parsed.data;
|
||||
|
||||
// Get the userId from contextMessage
|
||||
const userId: string = contextMessage.author.id;
|
||||
|
||||
// Find the action
|
||||
const action = actionsMap.get(actionId);
|
||||
if (!action) {
|
||||
return { error: `❌ Action with ID "${actionId}" not found.` };
|
||||
}
|
||||
|
||||
// Ensure the action belongs to the user
|
||||
if (action.userId !== userId) {
|
||||
return { error: `❌ You do not have permission to update this action.` };
|
||||
}
|
||||
|
||||
// Update fields
|
||||
action.description = description;
|
||||
action.schedule = schedule;
|
||||
action.instruction = instruction;
|
||||
action.template = template;
|
||||
action.notify = notify ?? action.notify;
|
||||
action.tool_names = tool_names;
|
||||
|
||||
actionsMap.set(actionId, action);
|
||||
await saveActionsToFile();
|
||||
|
||||
// Reschedule the action
|
||||
scheduleAction(action);
|
||||
|
||||
return {
|
||||
actionId,
|
||||
description,
|
||||
userId,
|
||||
schedule,
|
||||
instruction,
|
||||
template,
|
||||
tool_names,
|
||||
created_at: action.created_at,
|
||||
message: "✅ Action updated and rescheduled successfully.",
|
||||
};
|
||||
}
|
||||
|
||||
const action_tools: (
|
||||
context_message: Message
|
||||
) => RunnableToolFunctionWithParse<any>[] = (context_message) => [
|
||||
zodFunction({
|
||||
name: "create_action",
|
||||
function: (args) => create_action(args, context_message),
|
||||
schema: CreateActionParams,
|
||||
description: `Creates a new action.
|
||||
|
||||
**Example:**
|
||||
- **User:** "Send a summary email in 10 minutes"
|
||||
- **Action ID:** "send_summary_email"
|
||||
- **Description:** "Sends a summary email after a delay."
|
||||
- **Schedule:** { type: "delay", time: 600 }
|
||||
- **Instruction:** "Compose and send a summary email to the user."
|
||||
- **Required Tools:** ["email_service"]
|
||||
|
||||
**Notes:**
|
||||
- Supported scheduling types: 'delay' (in seconds), 'cron' (cron expressions).
|
||||
`,
|
||||
}),
|
||||
// zodFunction({
|
||||
// name: "get_actions",
|
||||
// function: (args) => get_actions(args, context_message),
|
||||
// schema: GetActionsParams,
|
||||
// description: `Retrieves all actions created by the user.
|
||||
|
||||
// Use this to obtain action IDs for updating or removing actions."
|
||||
// `,
|
||||
// }),
|
||||
zodFunction({
|
||||
name: "update_action",
|
||||
function: (args) => update_action(args, context_message),
|
||||
schema: UpdateActionParams,
|
||||
description: `Updates an existing action's details.
|
||||
|
||||
Provide all details of the action to replace it with the new parameters.
|
||||
`,
|
||||
}),
|
||||
zodFunction({
|
||||
name: "remove_action",
|
||||
function: (args) => remove_action(args),
|
||||
schema: RemoveActionParamsSchema,
|
||||
description: `Removes an action using the action ID.`,
|
||||
}),
|
||||
];
|
||||
|
||||
export const ActionManagerParamsSchema = z.object({
|
||||
request: z
|
||||
.string()
|
||||
.describe(
|
||||
"What the user wants you to do in the action. Please provide the time / schedule as well."
|
||||
),
|
||||
tool_names: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Names of the tools required to execute the instruction."),
|
||||
suggested_time_to_run_action: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ActionManagerParams = z.infer<typeof ActionManagerParamsSchema>;
|
||||
|
||||
// -------------------- Fuzzy Search for Actions -------------------- //
|
||||
|
||||
export const FuzzySearchActionsParams = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
export type FuzzySearchActionsParams = z.infer<typeof FuzzySearchActionsParams>;
|
||||
|
||||
export async function fuzzySearchActions({
|
||||
query,
|
||||
}: FuzzySearchActionsParams): Promise<{ matches: any[] }> {
|
||||
try {
|
||||
// Fetch all actions (Assuming get_actions is already defined and returns actions)
|
||||
const { actions } = await get_actions({}, {
|
||||
author: { id: "system" },
|
||||
} as any); // Replace with actual contextMessage if available
|
||||
|
||||
if (!actions) {
|
||||
return { matches: [] };
|
||||
}
|
||||
|
||||
const fuseOptions = {
|
||||
keys: ["description", "actionId"],
|
||||
threshold: 0.3, // Adjust the threshold as needed
|
||||
};
|
||||
|
||||
const fuse = new Fuse(actions, fuseOptions);
|
||||
const results = fuse.search(query);
|
||||
|
||||
// Get top 2 results
|
||||
const topMatches = results.slice(0, 2).map((result) => result.item);
|
||||
|
||||
return { matches: topMatches };
|
||||
} catch (error) {
|
||||
console.error("Error performing fuzzy search on actions:", error);
|
||||
return { matches: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Manager Function -------------------- //
|
||||
|
||||
/**
|
||||
* Manages user requests related to actions by orchestrating CRUD operations.
|
||||
* It can handle creating, updating, retrieving, and removing actions based on user input.
|
||||
*
|
||||
* @param params - Parameters containing the user's request, the action name, and an optional delay.
|
||||
* @returns A JSON object containing the response from executing the action or an error.
|
||||
*/
|
||||
export async function actionManager(
|
||||
{ request, tool_names, suggested_time_to_run_action }: ActionManagerParams,
|
||||
context_message: Message
|
||||
): Promise<any> {
|
||||
// Validate parameters using Zod
|
||||
const parsed = ActionManagerParamsSchema.safeParse({
|
||||
request,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
return { error: parsed.error.errors };
|
||||
}
|
||||
|
||||
const { request: userRequest } = parsed.data;
|
||||
|
||||
const all_actions = await get_actions({}, context_message);
|
||||
|
||||
const userConfigData = userConfigs.find((config) =>
|
||||
config.identities.find((id) => id.id === context_message.author.id)
|
||||
);
|
||||
|
||||
// Construct the prompt for the ask function
|
||||
const prompt = `You are an Action Manager.
|
||||
|
||||
Your role is to manage scheduled actions.
|
||||
|
||||
**Current Time:** ${new Date().toLocaleString()}
|
||||
|
||||
----
|
||||
|
||||
${memory_manager_guide("actions_manager")}
|
||||
|
||||
----
|
||||
|
||||
**Actions You Have Set Up for This User:**
|
||||
${JSON.stringify(all_actions.actions)}
|
||||
|
||||
**Current User Details:**
|
||||
${JSON.stringify(userConfigData)}
|
||||
|
||||
**Tools Suggested by user for Action:**
|
||||
${JSON.stringify(tool_names)}
|
||||
|
||||
---
|
||||
|
||||
Use the data provided above to fulfill the user's request.
|
||||
`;
|
||||
|
||||
const tools = action_tools(context_message).concat(
|
||||
memory_manager_init(context_message, "actions_manager")
|
||||
);
|
||||
|
||||
// console.log("Action Manager Tools:", tools);
|
||||
|
||||
// Execute the action using the ask function with the appropriate tools
|
||||
try {
|
||||
const response = await ask({
|
||||
prompt,
|
||||
message: `${userRequest}
|
||||
|
||||
Suggested time: ${suggested_time_to_run_action}
|
||||
`,
|
||||
seed: `action_manager_${context_message.channelId}`,
|
||||
tools,
|
||||
});
|
||||
|
||||
return {
|
||||
response: response.choices[0].message.content,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error executing action via manager:", error);
|
||||
return {
|
||||
error: "❌ An error occurred while executing your request.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize by loading actions from file when the module is loaded
|
||||
loadActionsFromFile();
|
|
@ -0,0 +1,394 @@
|
|||
import OpenAI from "openai";
|
||||
import { saveApiUsage } from "../usage";
|
||||
import axios from "axios";
|
||||
import fs from "fs";
|
||||
import { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
|
||||
import {
|
||||
ChatCompletion,
|
||||
ChatCompletionAssistantMessageParam,
|
||||
ChatCompletionMessageParam,
|
||||
} from "openai/resources/index.mjs";
|
||||
import { send_sys_log } from "../interfaces/log";
|
||||
import { pathInDataDir } from "../config";
|
||||
|
||||
const ai_token = process.env.OPENAI_API_KEY?.trim();
|
||||
const groq_token = process.env.GROQ_API_KEY?.trim();
|
||||
const groq_baseurl = process.env.GROQ_BASE_URL?.trim();
|
||||
|
||||
// Messages saving implementation
|
||||
|
||||
interface MessageHistory {
|
||||
messages: ChatCompletionMessageParam[];
|
||||
timeout: NodeJS.Timer;
|
||||
}
|
||||
|
||||
const seedMessageHistories: Map<string, MessageHistory> = new Map();
|
||||
const HISTORY_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Retrieves the message history for a given seed.
|
||||
* If it doesn't exist, initializes a new history.
|
||||
* Resets the timeout each time it's accessed.
|
||||
*
|
||||
* @param seed - The seed identifier for the message history
|
||||
* @returns The message history array
|
||||
*/
|
||||
function getMessageHistory(seed: string): ChatCompletionMessageParam[] {
|
||||
const existingHistory = seedMessageHistories.get(seed);
|
||||
|
||||
if (existingHistory) {
|
||||
// Reset the timeout
|
||||
clearTimeout(existingHistory.timeout);
|
||||
existingHistory.timeout = setTimeout(() => {
|
||||
seedMessageHistories.delete(seed);
|
||||
console.log(`Cleared message history for seed: ${seed}`);
|
||||
send_sys_log(`Cleared message history for seed: ${seed}`);
|
||||
}, HISTORY_TIMEOUT_MS);
|
||||
|
||||
return existingHistory.messages;
|
||||
} else {
|
||||
// Initialize new message history
|
||||
const messages: ChatCompletionMessageParam[] = [];
|
||||
const timeout = setTimeout(() => {
|
||||
seedMessageHistories.delete(seed);
|
||||
console.log(`Cleared message history for seed: ${seed}`);
|
||||
send_sys_log(`Cleared message history for seed: ${seed}`);
|
||||
}, HISTORY_TIMEOUT_MS);
|
||||
|
||||
seedMessageHistories.set(seed, { messages, timeout });
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the entire message history for a given seed.
|
||||
*
|
||||
* @param seed - The seed identifier for the message history
|
||||
* @param messages - The complete message history to set
|
||||
*/
|
||||
function setMessageHistory(
|
||||
seed: string,
|
||||
messages: ChatCompletionMessageParam[]
|
||||
): void {
|
||||
const existingHistory = seedMessageHistories.get(seed);
|
||||
if (existingHistory) {
|
||||
clearTimeout(existingHistory.timeout);
|
||||
existingHistory.messages = messages;
|
||||
existingHistory.timeout = setTimeout(() => {
|
||||
seedMessageHistories.delete(seed);
|
||||
console.log(`Cleared message history for seed: ${seed}`);
|
||||
send_sys_log(`Cleared message history for seed: ${seed}`);
|
||||
}, HISTORY_TIMEOUT_MS);
|
||||
} else {
|
||||
const timeout = setTimeout(() => {
|
||||
seedMessageHistories.delete(seed);
|
||||
console.log(`Cleared message history for seed: ${seed}`);
|
||||
send_sys_log(`Cleared message history for seed: ${seed}`);
|
||||
}, HISTORY_TIMEOUT_MS);
|
||||
seedMessageHistories.set(seed, { messages, timeout });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a message to the message history for a given seed.
|
||||
*
|
||||
* @param seed - The seed identifier for the message history
|
||||
* @param message - The message to append
|
||||
*/
|
||||
function appendMessage(
|
||||
seed: string,
|
||||
message: ChatCompletionMessageParam
|
||||
): void {
|
||||
console.log(
|
||||
"Appending message",
|
||||
message.content,
|
||||
"tool_calls" in message && message.tool_calls
|
||||
);
|
||||
|
||||
const history = seedMessageHistories.get(seed);
|
||||
if (history) {
|
||||
history.messages.push(message);
|
||||
// Reset the timeout
|
||||
clearTimeout(history.timeout);
|
||||
history.timeout = setTimeout(() => {
|
||||
seedMessageHistories.delete(seed);
|
||||
send_sys_log(`Cleared message history for seed: ${seed}`);
|
||||
console.log(`Cleared message history for seed: ${seed}`);
|
||||
}, HISTORY_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated ask function with support for persistent message history via a seed.
|
||||
* Separates system prompt and user message to prevent duplication.
|
||||
*
|
||||
* @param params - The parameters for the ask function
|
||||
* @returns The response from the LLM API
|
||||
*/
|
||||
export async function ask({
|
||||
model = "gpt-4o-mini",
|
||||
prompt, // System prompt
|
||||
message, // User input message (optional)
|
||||
name,
|
||||
tools,
|
||||
seed,
|
||||
}: {
|
||||
model?: string;
|
||||
prompt: string;
|
||||
message?: string;
|
||||
name?: string;
|
||||
tools?: RunnableToolFunctionWithParse<any>[];
|
||||
seed?: string;
|
||||
}): Promise<ChatCompletion> {
|
||||
// Initialize OpenAI instances
|
||||
const openai = new OpenAI({
|
||||
apiKey: ai_token,
|
||||
});
|
||||
|
||||
const groq = new OpenAI({
|
||||
apiKey: groq_token,
|
||||
baseURL: groq_baseurl,
|
||||
});
|
||||
|
||||
// Initialize messages array with the system prompt
|
||||
let messages: ChatCompletionMessageParam[] = [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
];
|
||||
|
||||
if (seed && message) {
|
||||
// Retrieve existing message history
|
||||
const history = getMessageHistory(seed);
|
||||
|
||||
// Combine system prompt with message history and new user message
|
||||
messages = [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
...history,
|
||||
{
|
||||
role: "user",
|
||||
content: message,
|
||||
name,
|
||||
},
|
||||
];
|
||||
} else if (seed && !message) {
|
||||
// If seed is provided but no new message, just retrieve history
|
||||
const history = getMessageHistory(seed);
|
||||
messages = [
|
||||
{
|
||||
role: "system",
|
||||
content: prompt,
|
||||
},
|
||||
...history,
|
||||
];
|
||||
} else if (!seed && message) {
|
||||
// If no seed but message is provided, send system prompt and user message without history
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: message,
|
||||
name,
|
||||
});
|
||||
}
|
||||
|
||||
let res: ChatCompletion;
|
||||
|
||||
if (model === "groq-small") {
|
||||
res = await groq.chat.completions.create({
|
||||
model: "llama-3.1-8b-instant",
|
||||
messages,
|
||||
});
|
||||
|
||||
if (res.usage) {
|
||||
saveApiUsage(
|
||||
new Date().toISOString().split("T")[0],
|
||||
model,
|
||||
res.usage.prompt_tokens,
|
||||
res.usage.completion_tokens
|
||||
);
|
||||
} else {
|
||||
console.log("No usage data");
|
||||
}
|
||||
|
||||
// Handle response with seed
|
||||
if (seed && res.choices && res.choices.length > 0) {
|
||||
appendMessage(seed, res.choices[0].message);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
if (tools?.length) {
|
||||
// Create a new runner with the current messages and tools
|
||||
const runner = openai.beta.chat.completions
|
||||
.runTools({
|
||||
model,
|
||||
messages,
|
||||
tools,
|
||||
})
|
||||
.on("functionCall", (functionCall) => {
|
||||
send_sys_log(`ASK Function call: ${JSON.stringify(functionCall)}`);
|
||||
console.log("ASK Function call:", functionCall);
|
||||
})
|
||||
.on("message", (message) => {
|
||||
// remove empty tool_calls array
|
||||
if (
|
||||
"tool_calls" in message &&
|
||||
message.tool_calls &&
|
||||
message.tool_calls.length === 0
|
||||
) {
|
||||
message.tool_calls = undefined;
|
||||
delete message.tool_calls;
|
||||
}
|
||||
seed && appendMessage(seed, message);
|
||||
})
|
||||
.on("totalUsage", (usage) => {
|
||||
send_sys_log(
|
||||
`ASK Total usage: ${usage.prompt_tokens} prompt tokens, ${usage.completion_tokens} completion tokens`
|
||||
);
|
||||
console.log("ASK Total usage:", usage);
|
||||
saveApiUsage(
|
||||
new Date().toISOString().split("T")[0],
|
||||
model,
|
||||
usage.prompt_tokens,
|
||||
usage.completion_tokens
|
||||
);
|
||||
});
|
||||
|
||||
// Await the final chat completion
|
||||
res = await runner.finalChatCompletion();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// Default behavior without tools
|
||||
res = await openai.chat.completions.create({
|
||||
model,
|
||||
messages,
|
||||
});
|
||||
|
||||
if (res.usage) {
|
||||
saveApiUsage(
|
||||
new Date().toISOString().split("T")[0],
|
||||
model,
|
||||
res.usage.prompt_tokens,
|
||||
res.usage.completion_tokens
|
||||
);
|
||||
} else {
|
||||
console.log("No usage data");
|
||||
}
|
||||
|
||||
// Handle response with seed
|
||||
if (seed && res.choices && res.choices.length > 0) {
|
||||
const assistantMessage = res.choices[0].message;
|
||||
appendMessage(seed, assistantMessage);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
const transcriptionCacheFile = pathInDataDir("transcription_cache.json");
|
||||
|
||||
export async function get_transcription(
|
||||
file_url: string,
|
||||
binary?: boolean,
|
||||
key?: string
|
||||
) {
|
||||
const openai = new OpenAI({
|
||||
apiKey: ai_token,
|
||||
});
|
||||
|
||||
// Step 1: Check if the transcription for this file URL is already cached
|
||||
let transcriptionCache: Record<string, string> = {};
|
||||
|
||||
// Try to read the cache file if it exists
|
||||
if (fs.existsSync(transcriptionCacheFile)) {
|
||||
const cacheData = fs.readFileSync(transcriptionCacheFile, "utf-8");
|
||||
transcriptionCache = JSON.parse(cacheData);
|
||||
}
|
||||
|
||||
if (binary) {
|
||||
// If transcription for this file_url is already in the cache, return it
|
||||
if (key && transcriptionCache[key]) {
|
||||
console.log("Transcription found in cache:", transcriptionCache[key]);
|
||||
return transcriptionCache[key];
|
||||
}
|
||||
|
||||
const binaryData = Buffer.from(file_url, "base64");
|
||||
// fs.writeFile("/home/audio_whats.ogg", binaryData, function (err) {});
|
||||
|
||||
const filePath = `/tmp/audio${Date.now()}.ogg`;
|
||||
|
||||
fs.writeFileSync(filePath, new Uint8Array(binaryData));
|
||||
|
||||
// Step 3: Send the file to OpenAI's Whisper model
|
||||
const transcription = await openai.audio.transcriptions.create({
|
||||
model: "whisper-1",
|
||||
file: fs.createReadStream(filePath),
|
||||
});
|
||||
|
||||
// Delete the temp file
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
// Step 4: Save the transcription to the cache
|
||||
key && (transcriptionCache[key] = transcription.text);
|
||||
fs.writeFileSync(
|
||||
transcriptionCacheFile,
|
||||
JSON.stringify(transcriptionCache, null, 2)
|
||||
);
|
||||
|
||||
console.log("Transcription:", transcription);
|
||||
|
||||
return transcription.text;
|
||||
}
|
||||
|
||||
// If transcription for this file_url is already in the cache, return it
|
||||
if (transcriptionCache[file_url]) {
|
||||
console.log("Transcription found in cache:", transcriptionCache[file_url]);
|
||||
return transcriptionCache[file_url];
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 2: Download the file from the URL
|
||||
const response = await axios({
|
||||
url: file_url,
|
||||
method: "GET",
|
||||
responseType: "stream",
|
||||
});
|
||||
|
||||
const filePath = `/tmp/audio${Date.now()}.ogg`;
|
||||
|
||||
// Save the downloaded file locally
|
||||
const writer = fs.createWriteStream(filePath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on("finish", resolve);
|
||||
writer.on("error", reject);
|
||||
});
|
||||
|
||||
// Step 3: Send the file to OpenAI's Whisper model
|
||||
const transcription = await openai.audio.transcriptions.create({
|
||||
model: "whisper-1",
|
||||
file: fs.createReadStream(filePath),
|
||||
});
|
||||
|
||||
// Delete the temp file
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
// Step 4: Save the transcription to the cache
|
||||
transcriptionCache[file_url] = transcription.text;
|
||||
fs.writeFileSync(
|
||||
transcriptionCacheFile,
|
||||
JSON.stringify(transcriptionCache, null, 2)
|
||||
);
|
||||
|
||||
console.log("Transcription:", transcription);
|
||||
return transcription.text;
|
||||
} catch (error) {
|
||||
console.error("Error transcribing audio:", error);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,556 @@
|
|||
import axios, { AxiosError } from "axios";
|
||||
import { z } from "zod";
|
||||
import { zodFunction } from ".";
|
||||
import { RunnableToolFunction } from "openai/lib/RunnableFunction.mjs";
|
||||
import { ask } from "./ask";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
|
||||
const NEXTCLOUD_API_ENDPOINT =
|
||||
"http://192.168.29.85/remote.php/dav/calendars/raj/";
|
||||
const NEXTCLOUD_USERNAME = process.env.NEXTCLOUD_USERNAME;
|
||||
const NEXTCLOUD_PASSWORD = process.env.NEXTCLOUD_PASSWORD;
|
||||
|
||||
if (!NEXTCLOUD_USERNAME || !NEXTCLOUD_PASSWORD) {
|
||||
throw new Error(
|
||||
"Please provide NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD environment variables."
|
||||
);
|
||||
}
|
||||
|
||||
const CALENDAR_NAME = "anya"; // Primary read-write calendar
|
||||
const READ_ONLY_CALENDARS = ["google"]; // Read-only calendar
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: NEXTCLOUD_API_ENDPOINT,
|
||||
auth: {
|
||||
username: NEXTCLOUD_USERNAME,
|
||||
password: NEXTCLOUD_PASSWORD,
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/xml", // Ensure correct content type for DAV requests
|
||||
},
|
||||
});
|
||||
|
||||
// Schemas for each function's parameters
|
||||
export const EventParams = z.object({
|
||||
event_id: z.string().describe("The unique ID of the event."),
|
||||
calendar: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The calendar the event belongs to."),
|
||||
});
|
||||
export type EventParams = z.infer<typeof EventParams>;
|
||||
|
||||
export const CreateEventParams = z.object({
|
||||
summary: z.string().describe("The summary (title) of the event."),
|
||||
description: z.string().optional().describe("The description of the event."),
|
||||
start_time: z
|
||||
.string()
|
||||
.describe("The start time of the event in ISO 8601 format."),
|
||||
end_time: z
|
||||
.string()
|
||||
.describe("The end time of the event in ISO 8601 format."),
|
||||
location: z.string().optional().describe("The location of the event."),
|
||||
attendees: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("List of attendee email addresses."),
|
||||
all_day: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether the event is an all-day event."),
|
||||
recurrence: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The recurrence rule for the event in RRULE format."),
|
||||
});
|
||||
export type CreateEventParams = z.infer<typeof CreateEventParams>;
|
||||
|
||||
export const UpdateEventParams = z.object({
|
||||
event_id: z.string().describe("The unique ID of the event to update."),
|
||||
summary: z.string().optional().describe("The updated summary of the event."),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated description of the event."),
|
||||
start_time: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated start time in ISO 8601 format."),
|
||||
end_time: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated end time in ISO 8601 format."),
|
||||
location: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated location of the event."),
|
||||
attendees: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Updated list of attendee email addresses."),
|
||||
calendar: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The calendar the event belongs to."),
|
||||
all_day: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether the event is an all-day event."),
|
||||
recurrence: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated recurrence rule for the event in RRULE format."),
|
||||
});
|
||||
export type UpdateEventParams = z.infer<typeof UpdateEventParams>;
|
||||
|
||||
function validateRecurrenceRule(recurrence: string): boolean {
|
||||
const rrulePattern =
|
||||
/^RRULE:(FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY);?(\w+=\w+;?)*)$/;
|
||||
return rrulePattern.test(recurrence);
|
||||
}
|
||||
|
||||
// Functions
|
||||
export async function createEvent({
|
||||
summary,
|
||||
description,
|
||||
start_time,
|
||||
end_time,
|
||||
location,
|
||||
attendees,
|
||||
all_day,
|
||||
recurrence,
|
||||
}: CreateEventParams) {
|
||||
const formatTime = (dateTime: string, allDay: boolean | undefined) => {
|
||||
if (allDay) {
|
||||
const date = new Date(dateTime);
|
||||
return `${date.getUTCFullYear()}${(date.getUTCMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}${date.getUTCDate().toString().padStart(2, "0")}`;
|
||||
} else {
|
||||
return formatDateTime(dateTime);
|
||||
}
|
||||
};
|
||||
|
||||
if (recurrence && !validateRecurrenceRule(recurrence)) {
|
||||
return { error: "Invalid recurrence rule syntax." };
|
||||
}
|
||||
|
||||
// Ensure DTEND for all-day events is correctly handled (should be the next day)
|
||||
const dtend = all_day
|
||||
? formatTime(end_time || start_time, all_day)
|
||||
: formatTime(end_time, all_day);
|
||||
|
||||
const icsContent = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//xrehpicx//anya//EN",
|
||||
"BEGIN:VEVENT",
|
||||
`UID:${Date.now()}@cloud.raj.how`,
|
||||
`SUMMARY:${summary}`,
|
||||
`DESCRIPTION:${description || ""}`,
|
||||
`DTSTART${all_day ? ";VALUE=DATE" : ""}:${formatTime(start_time, all_day)}`,
|
||||
`DTEND${all_day ? ";VALUE=DATE" : ""}:${dtend}`,
|
||||
`LOCATION:${location || ""}`,
|
||||
attendees
|
||||
? attendees
|
||||
.map((email) => `ATTENDEE;CN=${email}:mailto:${email}`)
|
||||
.join("\r\n")
|
||||
: "",
|
||||
recurrence || "",
|
||||
"END:VEVENT",
|
||||
"END:VCALENDAR",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\r\n");
|
||||
|
||||
try {
|
||||
const response = await apiClient.put(
|
||||
`${CALENDAR_NAME}/${Date.now()}.ics`,
|
||||
icsContent
|
||||
);
|
||||
return { response: "Event created successfully" };
|
||||
} catch (error) {
|
||||
console.log("Failed to create event:", error);
|
||||
console.log((error as AxiosError<any>).response?.data);
|
||||
return {
|
||||
error: `Error: ${error}\n${(error as AxiosError<any>).response?.data}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateEvent({
|
||||
event_id,
|
||||
summary,
|
||||
description,
|
||||
start_time,
|
||||
end_time,
|
||||
location,
|
||||
attendees,
|
||||
calendar = CALENDAR_NAME,
|
||||
all_day,
|
||||
recurrence,
|
||||
}: UpdateEventParams) {
|
||||
if (READ_ONLY_CALENDARS.includes(calendar)) {
|
||||
return { error: "This event is read-only and cannot be updated." };
|
||||
}
|
||||
|
||||
// Fetch the existing event to ensure we have all required data
|
||||
const existingEvent = await getEvent({ event_id, calendar });
|
||||
|
||||
if (existingEvent.error) {
|
||||
return { error: "Event not found" };
|
||||
}
|
||||
|
||||
// Determine whether the event is all-day, and format times accordingly
|
||||
const isAllDay = all_day !== undefined ? all_day : existingEvent.all_day;
|
||||
const formatTime = (
|
||||
dateTime: string | undefined,
|
||||
allDay: boolean
|
||||
): string => {
|
||||
if (!dateTime) return "";
|
||||
if (allDay) {
|
||||
const date = new Date(dateTime);
|
||||
return `${date.getUTCFullYear()}${(date.getUTCMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}${date.getUTCDate().toString().padStart(2, "0")}`;
|
||||
} else {
|
||||
return formatDateTime(dateTime);
|
||||
}
|
||||
};
|
||||
|
||||
if (recurrence && !validateRecurrenceRule(recurrence)) {
|
||||
return { error: "Invalid recurrence rule syntax." };
|
||||
}
|
||||
|
||||
// Format the ICS content, ensuring that all required fields are present
|
||||
const updatedICSContent = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//xrehpicx//anya//EN",
|
||||
"BEGIN:VEVENT",
|
||||
`UID:${event_id}`,
|
||||
`SUMMARY:${summary || existingEvent.summary}`,
|
||||
`DESCRIPTION:${description || existingEvent.description}`,
|
||||
`DTSTART${isAllDay ? ";VALUE=DATE" : ""}:${formatTime(
|
||||
start_time || existingEvent.start_time,
|
||||
isAllDay
|
||||
)}`,
|
||||
`DTEND${isAllDay ? ";VALUE=DATE" : ""}:${formatTime(
|
||||
end_time || existingEvent.end_time,
|
||||
isAllDay
|
||||
)}`,
|
||||
`LOCATION:${location || existingEvent.location}`,
|
||||
attendees
|
||||
? attendees
|
||||
.map((email) => `ATTENDEE;CN=${email}:mailto:${email}`)
|
||||
.join("\r\n")
|
||||
: existingEvent.attendees,
|
||||
recurrence || existingEvent.recurrence,
|
||||
"END:VEVENT",
|
||||
"END:VCALENDAR",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\r\n");
|
||||
|
||||
try {
|
||||
const response = await apiClient.put(
|
||||
`${calendar}/${event_id}.ics`,
|
||||
updatedICSContent
|
||||
);
|
||||
|
||||
return { response: "Event updated successfully" };
|
||||
} catch (error) {
|
||||
console.log("Failed to update event:", error);
|
||||
console.log((error as AxiosError<any>).response?.data);
|
||||
return {
|
||||
error: `Error: ${error}\n${(error as AxiosError<any>).response?.data}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteEvent({ event_id }: EventParams) {
|
||||
try {
|
||||
const deleteUrl = `${NEXTCLOUD_API_ENDPOINT}${CALENDAR_NAME}/${event_id}.ics`; // Correctly form the URL
|
||||
console.log(`Attempting to delete event at: ${deleteUrl}`);
|
||||
|
||||
const response = await apiClient.delete(deleteUrl);
|
||||
console.log("Event deleted successfully:", response.status);
|
||||
return { response: "Event deleted successfully" };
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Failed to delete event:",
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
);
|
||||
return {
|
||||
error: `Error: ${
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEvent({
|
||||
event_id,
|
||||
calendar = CALENDAR_NAME,
|
||||
}: EventParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`${calendar}/${event_id}.ics`);
|
||||
const eventData = response.data;
|
||||
const isReadOnly = READ_ONLY_CALENDARS.includes(calendar);
|
||||
|
||||
return {
|
||||
...eventData,
|
||||
read_only: isReadOnly ? "Read-Only" : "Editable",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
error: `Error: ${error}\n${(error as AxiosError<any>).response?.data}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function listEvents({
|
||||
start_time,
|
||||
end_time,
|
||||
}: {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
}) {
|
||||
try {
|
||||
const startISOTime = convertToISOFormat(start_time);
|
||||
const endISOTime = convertToISOFormat(end_time);
|
||||
|
||||
const allEvents: any[] = [];
|
||||
|
||||
for (const calendar of [CALENDAR_NAME, ...READ_ONLY_CALENDARS]) {
|
||||
const calendarUrl = `${NEXTCLOUD_API_ENDPOINT}${calendar}/`;
|
||||
console.log(`Accessing calendar URL: ${calendarUrl}`);
|
||||
|
||||
try {
|
||||
const testResponse = await apiClient.get(calendarUrl);
|
||||
console.log(`Test response for ${calendarUrl}: ${testResponse.status}`);
|
||||
} catch (testError) {
|
||||
console.error(
|
||||
`Error accessing ${calendarUrl}: ${(testError as AxiosError).message}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Making REPORT request to ${calendarUrl} for events between ${startISOTime} and ${endISOTime}`
|
||||
);
|
||||
|
||||
let reportResponse;
|
||||
try {
|
||||
reportResponse = await apiClient.request({
|
||||
method: "REPORT",
|
||||
url: calendarUrl,
|
||||
headers: { Depth: "1" },
|
||||
data: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<calendar-query xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<calendar-data/>
|
||||
<filter>
|
||||
<comp-filter name="VCALENDAR">
|
||||
<comp-filter name="VEVENT">
|
||||
<time-range start="${startISOTime}" end="${endISOTime}"/>
|
||||
</comp-filter>
|
||||
</comp-filter>
|
||||
</filter>
|
||||
</calendar-query>`,
|
||||
});
|
||||
console.log(
|
||||
`REPORT request successful: Status ${reportResponse.status}`
|
||||
);
|
||||
} catch (reportError) {
|
||||
console.error(
|
||||
`REPORT request failed for ${calendarUrl}: ${
|
||||
(reportError as AxiosError).response?.data ||
|
||||
(reportError as AxiosError).message
|
||||
}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Parsing iCal response for calendar ${calendar}`);
|
||||
const icsFiles = parseICalResponse(reportResponse.data);
|
||||
|
||||
for (const icsFile of icsFiles) {
|
||||
const fullIcsUrl = `http://192.168.29.85${icsFile}?export`;
|
||||
const eventId = icsFile.split("/").pop()?.replace(".ics", "");
|
||||
console.log(`Fetching event data from ${fullIcsUrl}`);
|
||||
|
||||
try {
|
||||
const eventResponse = await apiClient.get(fullIcsUrl, {
|
||||
responseType: "text",
|
||||
});
|
||||
const eventData = eventResponse.data;
|
||||
|
||||
allEvents.push({
|
||||
event_id: eventId, // Add event ID explicitly here
|
||||
data: eventData,
|
||||
calendar,
|
||||
read_only: READ_ONLY_CALENDARS.includes(calendar)
|
||||
? "Read-Only"
|
||||
: "Editable",
|
||||
});
|
||||
console.log(`Event data fetched successfully from ${fullIcsUrl}`);
|
||||
} catch (eventError) {
|
||||
console.error(
|
||||
`Failed to fetch event data from ${fullIcsUrl}: ${
|
||||
(eventError as AxiosError).response?.data ||
|
||||
(eventError as AxiosError).message
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allEvents;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Final catch block error:",
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
);
|
||||
return {
|
||||
error: `Error: ${
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert datetime to ISO format in UTC
|
||||
function convertToISOFormat(dateTime: string): string {
|
||||
const date = new Date(dateTime);
|
||||
return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
||||
}
|
||||
|
||||
// Helper function to parse iCal response
|
||||
function parseICalResponse(response: string): string[] {
|
||||
const hrefRegex = /<d:href>([^<]+)<\/d:href>/g;
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = hrefRegex.exec(response)) !== null) {
|
||||
matches.push(match[1]);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Helper function to format datetime for iCalendar
|
||||
function formatDateTime(dateTime: string): string {
|
||||
const date = new Date(dateTime);
|
||||
const year = date.getUTCFullYear().toString().padStart(4, "0");
|
||||
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getUTCDate().toString().padStart(2, "0");
|
||||
const hours = date.getUTCHours().toString().padStart(2, "0");
|
||||
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
|
||||
const seconds = date.getUTCSeconds().toString().padStart(2, "0");
|
||||
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`; // UTC format required by iCalendar
|
||||
}
|
||||
|
||||
// Integration into runnable tools
|
||||
export let calendar_tools: RunnableToolFunction<any>[] = [
|
||||
zodFunction({
|
||||
function: createEvent,
|
||||
name: "createCalendarEvent",
|
||||
schema: CreateEventParams,
|
||||
description: "Create a new event in the 'anya' calendar.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: updateEvent,
|
||||
name: "updateCalendarEvent",
|
||||
schema: UpdateEventParams,
|
||||
description:
|
||||
"Update an event in the 'anya' calendar. Cannot update read-only events.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: deleteEvent,
|
||||
name: "deleteCalendarEvent",
|
||||
schema: EventParams,
|
||||
description:
|
||||
"Delete an event from the 'anya' calendar. Cannot delete read-only events.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getEvent,
|
||||
name: "getCalendarEvent",
|
||||
schema: EventParams,
|
||||
description:
|
||||
"Retrieve an event from the 'anya' calendar. Indicates if the event is read-only.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: listEvents,
|
||||
name: "listCalendarEvents",
|
||||
schema: z.object({
|
||||
start_time: z.string().describe("Start time in ISO 8601 format."),
|
||||
end_time: z.string().describe("End time in ISO 8601 format."),
|
||||
}),
|
||||
description:
|
||||
"List events within a time range from the 'anya' calendar, including read-only events.",
|
||||
}),
|
||||
];
|
||||
|
||||
export function getCalendarSystemPrompt() {
|
||||
return `Manage your 'anya' calendar on Nextcloud using these functions to create, update, delete, and list events.
|
||||
|
||||
Read-only events cannot be updated or deleted; they are labeled as "Read-Only" when retrieved or listed.
|
||||
|
||||
Use correct ISO 8601 time formats and handle event IDs carefully.
|
||||
|
||||
**Do not use this for reminders.**
|
||||
|
||||
User's primary emails: r@raj.how and raj@cloud.raj.how
|
||||
|
||||
When creating or updating an event, inform the user of the event date or details updated.
|
||||
`;
|
||||
}
|
||||
|
||||
export const CalendarManagerParams = z.object({
|
||||
request: z.string().describe("User's request regarding calendar events."),
|
||||
});
|
||||
export type CalendarManagerParams = z.infer<typeof CalendarManagerParams>;
|
||||
|
||||
export async function calendarManager(
|
||||
{ request }: CalendarManagerParams,
|
||||
context_message: Message
|
||||
) {
|
||||
// Set start and end dates for listing events
|
||||
const startDate = new Date();
|
||||
startDate.setDate(1);
|
||||
startDate.setMonth(startDate.getMonth() - 1);
|
||||
const endDate = new Date();
|
||||
endDate.setDate(1);
|
||||
endDate.setMonth(endDate.getMonth() + 2);
|
||||
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `You are a calendar manager for the 'anya' calendar on Nextcloud.
|
||||
|
||||
Understand the user's request regarding calendar events (create, update, delete, list) and handle it using available tools.
|
||||
|
||||
Use correct ISO 8601 time formats. Provide feedback about actions taken, including event dates or details updated.
|
||||
|
||||
User's primary emails: r@raj.how and raj@cloud.raj.how. Inform the user of the date of any created or updated event.
|
||||
|
||||
----
|
||||
${memory_manager_guide("calendar_manager")}
|
||||
----
|
||||
|
||||
Current Time: ${new Date().toISOString()}
|
||||
|
||||
Events from ${startDate.toISOString()} to ${endDate.toISOString()}:
|
||||
${await listEvents({
|
||||
start_time: startDate.toISOString(),
|
||||
end_time: endDate.toISOString(),
|
||||
})}
|
||||
`,
|
||||
tools: calendar_tools.concat(
|
||||
memory_manager_init(context_message, "calendar_manager")
|
||||
) as any,
|
||||
message: request,
|
||||
seed: `calendar-manager-${context_message.channelId}`,
|
||||
});
|
||||
|
||||
return { response };
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const GenerateCatImageUrlParams = z.object({
|
||||
tag: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
fontSize: z.number().optional(),
|
||||
fontColor: z.string().optional(),
|
||||
gif: z.boolean().optional(),
|
||||
});
|
||||
export type GenerateCatImageUrlParams = z.infer<
|
||||
typeof GenerateCatImageUrlParams
|
||||
>;
|
||||
|
||||
export const GenerateCatImageUrlResponse = z.string();
|
||||
export type GenerateCatImageUrlResponse = z.infer<
|
||||
typeof GenerateCatImageUrlResponse
|
||||
>;
|
||||
|
||||
export async function generate_cat_image_url({
|
||||
tag,
|
||||
text,
|
||||
fontSize,
|
||||
fontColor,
|
||||
gif,
|
||||
}: GenerateCatImageUrlParams): Promise<object> {
|
||||
let url = "/cat";
|
||||
|
||||
if (gif) {
|
||||
url += "/gif";
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
url += `/${tag}`;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
url += `/says/${text}`;
|
||||
}
|
||||
|
||||
if (fontSize || fontColor) {
|
||||
if (!text) {
|
||||
url += `/says/`; // Ensuring 'says' is in the URL for font options
|
||||
}
|
||||
url += `${text ? "" : "?"}`;
|
||||
if (fontSize) {
|
||||
url += `fontSize=${fontSize}`;
|
||||
}
|
||||
if (fontColor) {
|
||||
url += `${fontSize ? "&" : ""}fontColor=${fontColor}`;
|
||||
}
|
||||
}
|
||||
|
||||
return { url };
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// tools/chat-search.ts
|
||||
|
||||
import { z } from "zod";
|
||||
import fuzzysort from "fuzzysort";
|
||||
import { Message } from "../interfaces/message";
|
||||
|
||||
// Define the search parameters schema
|
||||
export const SearchChatParams = z.object({
|
||||
query: z.string(),
|
||||
k: z.number().max(100).default(5).optional(),
|
||||
limit: z.number().max(100).default(100).optional(),
|
||||
user_only: z.boolean().default(false).optional(),
|
||||
});
|
||||
export type SearchChatParams = z.infer<typeof SearchChatParams>;
|
||||
|
||||
// Function to search chat messages
|
||||
export async function search_chat(
|
||||
{ query, k = 5, limit = 100, user_only = false }: SearchChatParams,
|
||||
context_message: Message
|
||||
) {
|
||||
// Fetch recent messages from the current channel
|
||||
const messages = await context_message.fetchChannelMessages(limit);
|
||||
|
||||
// Filter messages if user_only is true
|
||||
const filteredMessages = user_only
|
||||
? messages.filter((msg) => msg.author.id === context_message.author.id)
|
||||
: messages;
|
||||
|
||||
// Prepare list for fuzzysort
|
||||
const list = filteredMessages.map((msg) => ({
|
||||
message: msg,
|
||||
content: msg.content,
|
||||
}));
|
||||
|
||||
// Perform fuzzy search on message contents
|
||||
const results = fuzzysort.go(query, list, { key: "content", limit: k });
|
||||
|
||||
// Map results back to messages
|
||||
const matchedMessages = results.map((result) => {
|
||||
const matchedMessage = result.obj.message;
|
||||
return {
|
||||
content: matchedMessage.content,
|
||||
author: matchedMessage.author.username,
|
||||
timestamp: matchedMessage.timestamp,
|
||||
id: matchedMessage.id,
|
||||
};
|
||||
});
|
||||
|
||||
return { results: matchedMessages };
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
import { z } from "zod";
|
||||
import { zodFunction } from ".";
|
||||
import { send_message_to, SendMessageParams } from "./messenger";
|
||||
import { send_email, ResendParams } from "./resend";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { search_user, SearchUserParams } from "./search-user";
|
||||
import { RunnableToolFunctionWithParse } from "openai/lib/RunnableFunction.mjs";
|
||||
import { ask } from "./ask";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
import { userConfigs } from "../config";
|
||||
|
||||
const CommunicationManagerSchema = z.object({
|
||||
request: z.string(),
|
||||
prefered_platform: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"The platform you prefer to use, you can leave this empty to default to the current user's platform."
|
||||
),
|
||||
prefered_recipient_details: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
user_id: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
.describe("Give these details only if you have them."),
|
||||
});
|
||||
|
||||
export type CommunicationManager = z.infer<typeof CommunicationManagerSchema>;
|
||||
|
||||
const communication_tools = (context_message: Message) => {
|
||||
const allTools: RunnableToolFunctionWithParse<any>[] = [
|
||||
zodFunction({
|
||||
function: (args) => search_user(args, context_message),
|
||||
name: "search_user",
|
||||
schema: SearchUserParams,
|
||||
description: `Retrieve a user's details (email or platform IDs) by searching their name.
|
||||
|
||||
Supported platforms: ['whatsapp', 'discord', 'email', 'events']`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: (args) => send_message_to(args, context_message),
|
||||
name: "send_message_to",
|
||||
schema: SendMessageParams,
|
||||
description: `Send a message to a user or relation using their config name or user ID.
|
||||
|
||||
- **Current user's platform:** ${context_message.platform}
|
||||
- If no platform is specified, use the current user's platform unless specified otherwise.
|
||||
- If no \`user_name\` is provided, the message will be sent to the current user.
|
||||
- Use \`search_user\` to obtain the \`user_id\`.
|
||||
- Supported platforms: ['whatsapp', 'discord']
|
||||
|
||||
**Note:** When sending a message on behalf of someone else, mention who is sending it. For example, if Pooja asks you to remind Raj to drink water, send: "Pooja wanted to remind you to drink water."`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: send_email,
|
||||
schema: ResendParams,
|
||||
description: `Send an email to a specified email address.
|
||||
|
||||
- Confirm the recipient's email with the user before sending.
|
||||
- Use \`search_user\` to get the email if only a name is provided.
|
||||
- Do not invent an email address if none is found.`,
|
||||
}),
|
||||
];
|
||||
|
||||
return allTools;
|
||||
};
|
||||
|
||||
export async function communication_manager(
|
||||
{
|
||||
request,
|
||||
prefered_platform,
|
||||
prefered_recipient_details,
|
||||
}: CommunicationManager,
|
||||
context_message: Message
|
||||
) {
|
||||
const tools = communication_tools(context_message).concat(
|
||||
memory_manager_init(context_message, "communications_manager")
|
||||
);
|
||||
|
||||
const prompt = `You are a Communication Manager Tool.
|
||||
|
||||
Your task is to route messages to the correct recipient.
|
||||
|
||||
It is extremely important that the right message goes to the right user, and never to the wrong user.
|
||||
|
||||
---
|
||||
|
||||
${memory_manager_guide("communications_manager")}
|
||||
|
||||
You can use the \`memory_manager\` tool to remember user preferences, such as what the user calls certain contacts, to help you route messages better.
|
||||
|
||||
---
|
||||
|
||||
**Default Platform (if not mentioned):** ${context_message.platform}
|
||||
|
||||
**Configuration of All Users:** ${JSON.stringify(userConfigs)}
|
||||
|
||||
**Can Access 'WhatsApp':** ${context_message.getUserRoles().includes("creator")}
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- If the user does not mention a platform, use the same platform as the current user.
|
||||
|
||||
- Look for the recipient's details in the user configuration before checking WhatsApp users.
|
||||
|
||||
- If the recipient is not on the current user's platform and the user can access WhatsApp, you may check if the recipient is on WhatsApp. Confirm the WhatsApp number (WhatsApp ID) with the user before sending the message.
|
||||
|
||||
- Check WhatsApp only if the user can access it and the recipient is not found in the user config or if the user explicitly asks to send the message on WhatsApp.
|
||||
`;
|
||||
|
||||
const response = await ask({
|
||||
prompt,
|
||||
message: `request: ${request}
|
||||
|
||||
prefered_platform: ${prefered_platform}
|
||||
|
||||
prefered_recipient_details: ${JSON.stringify(prefered_recipient_details)}`,
|
||||
tools,
|
||||
});
|
||||
|
||||
try {
|
||||
return {
|
||||
response: response.choices[0].message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const communication_manager_tool = (context_message: Message) =>
|
||||
zodFunction({
|
||||
function: (args) => communication_manager(args, context_message),
|
||||
name: "communication_manager",
|
||||
schema: CommunicationManagerSchema,
|
||||
description: `Communications Manager.
|
||||
|
||||
This tool routes messages to the specified user on the appropriate platform.
|
||||
|
||||
Use it to send messages to users on various platforms.
|
||||
|
||||
Provide detailed information to ensure the message reaches the correct recipient.
|
||||
|
||||
Include in your request the message content and the recipient's details.
|
||||
|
||||
**Example:**
|
||||
|
||||
- **User:** "Tell Pooja to call me."
|
||||
- **Sender's Name:** Raj
|
||||
- **Recipient's Name:** Pooja
|
||||
- **Generated Request String:** "Raj wants to message Pooja 'call me'. Seems like he's in a hurry, so you can format it to sound urgent."
|
||||
`,
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
|
||||
// run bash command in docker container
|
||||
export const RunCommandParams = z.object({
|
||||
command: z.string().describe("the command to run"),
|
||||
});
|
||||
export type RunCommandParams = z.infer<typeof RunCommandParams>;
|
||||
export async function run_command({ command }: RunCommandParams) {
|
||||
const { exec } = require("child_process");
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error: any, stdout: any, stderr: any) => {
|
||||
if (error) {
|
||||
console.log(`error: ${error.message}`);
|
||||
reject(error.message);
|
||||
}
|
||||
if (stderr) {
|
||||
console.log(`stderr: ${stderr}`);
|
||||
reject(stderr);
|
||||
}
|
||||
console.log(`stdout: ${stdout}`);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,75 @@
|
|||
import { Client } from "minio";
|
||||
import { z } from "zod";
|
||||
|
||||
if (!process.env.MINIO_ACCESS_KEY || !process.env.MINIO_SECRET_KEY) {
|
||||
throw new Error(
|
||||
"MINIO_ACCESS_KEY or MINIO_SECRET_KEY not found in environment variables"
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize MinIO client
|
||||
const minioClient = new Client({
|
||||
endPoint: "s3.raj.how",
|
||||
port: 443,
|
||||
useSSL: true,
|
||||
accessKey: process.env.MINIO_ACCESS_KEY,
|
||||
secretKey: process.env.MINIO_SECRET_KEY,
|
||||
});
|
||||
|
||||
// Define schema for uploading file
|
||||
export const UploadFileParams = z.object({
|
||||
bucketName: z.string().default("public").optional(),
|
||||
fileName: z.string().describe("make sure this is unique"),
|
||||
filePath: z
|
||||
.string()
|
||||
.describe(
|
||||
"put all files inside 'anya' directory by default unless user specifies otherwise"
|
||||
),
|
||||
});
|
||||
export type UploadFileParams = z.infer<typeof UploadFileParams>;
|
||||
|
||||
// Define schema for getting file list
|
||||
export const GetFileListParams = z.object({
|
||||
bucketName: z.string().default("public").optional(),
|
||||
});
|
||||
export type GetFileListParams = z.infer<typeof GetFileListParams>;
|
||||
|
||||
// Upload file to MinIO bucket and return public URL
|
||||
export async function upload_file({
|
||||
bucketName = "public",
|
||||
fileName,
|
||||
filePath,
|
||||
}: UploadFileParams) {
|
||||
try {
|
||||
await minioClient.fPutObject(bucketName, fileName, filePath);
|
||||
const publicUrl = `https://s3.raj.how/${bucketName}/${fileName}`;
|
||||
return {
|
||||
publicUrl,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error: JSON.stringify(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get list of all files in the bucket and return their public URLs
|
||||
export async function get_file_list({
|
||||
bucketName = "public",
|
||||
}: GetFileListParams) {
|
||||
try {
|
||||
const fileUrls: string[] = [];
|
||||
const stream = await minioClient.listObjects(bucketName, "", true);
|
||||
|
||||
for await (const obj of stream) {
|
||||
const fileUrl = `https://s3.raj.how/${bucketName}/${obj.name}`;
|
||||
fileUrls.push(fileUrl);
|
||||
}
|
||||
|
||||
return fileUrls;
|
||||
} catch (error) {
|
||||
return {
|
||||
error: JSON.stringify(error),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { getJson } from "serpapi";
|
||||
import { z } from "zod";
|
||||
|
||||
export const GoogleSearchParams = z.object({
|
||||
query: z.string(),
|
||||
engine: z
|
||||
.enum([
|
||||
"google_news",
|
||||
"google_scholar",
|
||||
"google_images",
|
||||
"google_flights",
|
||||
"google_jobs",
|
||||
"google_videos",
|
||||
"google_local",
|
||||
"google_maps",
|
||||
"google_shopping",
|
||||
])
|
||||
.describe("search engine"),
|
||||
type: z
|
||||
.enum([
|
||||
"news_results",
|
||||
"organic_results",
|
||||
"local_results",
|
||||
"knowledge_graph",
|
||||
"recipes_results",
|
||||
"shopping_results",
|
||||
"jobs_results",
|
||||
"inline_videos",
|
||||
"inline_images",
|
||||
"all",
|
||||
])
|
||||
.describe("type of results that correspond the selected search engine"),
|
||||
n: z.number().optional().describe("number of results"),
|
||||
});
|
||||
export type GoogleSearchParams = z.infer<typeof GoogleSearchParams>;
|
||||
export async function search({ query, type, n, engine }: GoogleSearchParams) {
|
||||
if (!process.env.SEARCH_API_KEY)
|
||||
return { response: "missing SEARCH_API_KEY env var" };
|
||||
|
||||
const res = await getJson({
|
||||
engine: engine ?? "google",
|
||||
q: query,
|
||||
api_key: process.env.SEARCH_API_KEY,
|
||||
num: n,
|
||||
});
|
||||
|
||||
if (type === "all") {
|
||||
return res;
|
||||
}
|
||||
|
||||
if (res[type]) {
|
||||
return res[type];
|
||||
}
|
||||
return {
|
||||
response: `no results, for the specified type ${type}, try a different type maybe`,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,393 @@
|
|||
import { z } from "zod";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { zodFunction } from ".";
|
||||
import {
|
||||
RunnableToolFunction,
|
||||
RunnableToolFunctionWithParse,
|
||||
} from "openai/lib/RunnableFunction.mjs";
|
||||
import Fuse from "fuse.js";
|
||||
import { ask } from "./ask";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
|
||||
// Global axios config for Home Assistant API
|
||||
const homeAssistantUrl = "https://home.raj.how";
|
||||
const token = process.env.HA_KEY;
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: `${homeAssistantUrl}/api`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------- Caching Utility -------------------- //
|
||||
|
||||
type CacheEntry<T> = {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
class AsyncCache<T> {
|
||||
private cache: CacheEntry<T> | null = null;
|
||||
private fetchFunction: () => Promise<T>;
|
||||
private refreshing: boolean = false;
|
||||
private refreshInterval: number; // in milliseconds
|
||||
|
||||
constructor(
|
||||
fetchFunction: () => Promise<T>,
|
||||
refreshInterval: number = 5 * 60 * 1000
|
||||
) {
|
||||
// Default refresh interval: 5 minutes
|
||||
this.fetchFunction = fetchFunction;
|
||||
this.refreshInterval = refreshInterval;
|
||||
}
|
||||
|
||||
async get(): Promise<T> {
|
||||
if (this.cache) {
|
||||
// Return cached data immediately
|
||||
this.refreshInBackground();
|
||||
return this.cache.data;
|
||||
} else {
|
||||
// No cache available, fetch data and cache it
|
||||
const data = await this.fetchFunction();
|
||||
this.cache = { data, timestamp: Date.now() };
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshInBackground() {
|
||||
if (this.refreshing) return; // Prevent multiple simultaneous refreshes
|
||||
this.refreshing = true;
|
||||
|
||||
// Perform the refresh without blocking the main thread
|
||||
this.fetchFunction()
|
||||
.then((data) => {
|
||||
this.cache = { data, timestamp: Date.now() };
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error refreshing cache:", error);
|
||||
// Optionally, handle the error (e.g., keep the old cache)
|
||||
})
|
||||
.finally(() => {
|
||||
this.refreshing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Memoized Functions -------------------- //
|
||||
|
||||
// 1. Fetch all available services with caching
|
||||
export async function getAllServicesRaw() {
|
||||
try {
|
||||
const response = await apiClient.get("/services");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching services:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const servicesCache = new AsyncCache<any[]>(getAllServicesRaw);
|
||||
|
||||
export async function getAllServices() {
|
||||
try {
|
||||
const services = await servicesCache.get();
|
||||
return services;
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fetch all devices and their valid states and services with caching
|
||||
export async function getAllDevicesRaw() {
|
||||
try {
|
||||
const response = await apiClient.get("/states");
|
||||
const services = await getAllServicesRaw();
|
||||
|
||||
const devices = response.data.map((device: any) => {
|
||||
const domain = device.entity_id.split(".")[0];
|
||||
|
||||
// Find valid services for this entity's domain
|
||||
const domainServices =
|
||||
services.find((service: any) => service.domain === domain)?.services ||
|
||||
[];
|
||||
|
||||
return {
|
||||
entity_id: device.entity_id,
|
||||
state: device.state,
|
||||
friendly_name: device.attributes.friendly_name || "",
|
||||
valid_services: domainServices, // Add the valid services for this device
|
||||
attributes: {
|
||||
valid_states: device.attributes.valid_states || [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return devices;
|
||||
} catch (error) {
|
||||
console.error("Error fetching devices:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const devicesCache = new AsyncCache<any[]>(getAllDevicesRaw);
|
||||
|
||||
export async function getAllDevices() {
|
||||
try {
|
||||
const devices = await devicesCache.get();
|
||||
return { devices };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- Existing Functionality -------------------- //
|
||||
|
||||
// Schema for setting the state with service and optional parameters
|
||||
export const SetDeviceStateParams = z.object({
|
||||
entity_id: z.string(),
|
||||
service: z.string(), // Taking service directly
|
||||
value: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"The value to set for the service. use this for simple use cases like for setting text, for more complex use cases use params"
|
||||
),
|
||||
params: z
|
||||
.object({})
|
||||
.optional()
|
||||
.describe(
|
||||
`This object contains optional parameters for the service. For example, you can pass the brightness, color, or other parameters specific to the service.`
|
||||
), // Optional parameters for the service (e.g., brightness, color)
|
||||
});
|
||||
export type SetDeviceStateParams = z.infer<typeof SetDeviceStateParams>;
|
||||
|
||||
// Schema for fuzzy search
|
||||
export const FuzzySearchParams = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
export type FuzzySearchParams = z.infer<typeof FuzzySearchParams>;
|
||||
|
||||
// 3. Fuzzy search devices and include valid services
|
||||
export async function fuzzySearchDevices({ query }: FuzzySearchParams) {
|
||||
try {
|
||||
// Fetch all devices with their services
|
||||
const { devices }: any = await getAllDevices();
|
||||
|
||||
if (!devices) {
|
||||
return { error: "No devices data available." };
|
||||
}
|
||||
|
||||
const fuseOptions = {
|
||||
keys: ["friendly_name", "entity_id"],
|
||||
threshold: 0.3, // Controls the fuzziness, lower value means stricter match
|
||||
};
|
||||
|
||||
const fuse = new Fuse(devices, fuseOptions);
|
||||
const results = fuse.search(query);
|
||||
|
||||
// Get top 2 results
|
||||
const topMatches = results.slice(0, 2).map((result) => result.item);
|
||||
|
||||
return { matches: topMatches };
|
||||
} catch (error) {
|
||||
console.error("Error performing fuzzy search:", error);
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Function to set the state of a device via a service
|
||||
|
||||
// Updated setDeviceState function
|
||||
export async function setDeviceState({
|
||||
entity_id,
|
||||
service,
|
||||
value,
|
||||
params = {},
|
||||
}: SetDeviceStateParams) {
|
||||
try {
|
||||
const domain = entity_id.split(".")[0];
|
||||
|
||||
// Fetch valid services for the specific domain
|
||||
const valid_services = await getServicesForDomain(domain);
|
||||
|
||||
// Ensure valid_services is an object and extract the service keys
|
||||
const valid_service_keys = valid_services
|
||||
? Object.keys(valid_services)
|
||||
: [];
|
||||
|
||||
// Check if the passed service is valid
|
||||
if (!valid_service_keys.includes(service)) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Invalid service '${service}' for entity ${entity_id}. Valid services are: ${valid_service_keys.join(
|
||||
", "
|
||||
)}.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!params && !value) {
|
||||
return {
|
||||
success: false,
|
||||
message: `No value or params provided for service '${service}' for entity ${entity_id}.`,
|
||||
};
|
||||
}
|
||||
|
||||
// Send a POST request to the appropriate service endpoint with optional parameters
|
||||
const response = await apiClient.post(`/services/${domain}/${service}`, {
|
||||
entity_id,
|
||||
value,
|
||||
...params,
|
||||
});
|
||||
|
||||
return { success: response.status === 200 };
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
const errMessage = err.response?.data || { message: err.message };
|
||||
console.error(
|
||||
`Error setting state for device ${entity_id}:`,
|
||||
JSON.stringify(errMessage, null, 2)
|
||||
);
|
||||
return { errMessage };
|
||||
}
|
||||
}
|
||||
|
||||
// Schema for getting device state
|
||||
export const GetDeviceStateParams = z.object({
|
||||
entity_id: z.string(),
|
||||
});
|
||||
export type GetDeviceStateParams = z.infer<typeof GetDeviceStateParams>;
|
||||
|
||||
// Fetch services for a specific domain (e.g., light, switch)
|
||||
async function getServicesForDomain(domain: string) {
|
||||
try {
|
||||
const services = await getAllServices();
|
||||
if ("error" in services) throw services.error;
|
||||
|
||||
const domainServices = services.find(
|
||||
(service: any) => service.domain === domain
|
||||
);
|
||||
return domainServices ? domainServices.services : [];
|
||||
} catch (error) {
|
||||
console.error(`Error fetching services for domain ${domain}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get the current state and valid services of a specific device
|
||||
export async function getDeviceState({ entity_id }: GetDeviceStateParams) {
|
||||
try {
|
||||
// Fetch the device state
|
||||
const response = await apiClient.get(`/states/${entity_id}`);
|
||||
const device = response.data;
|
||||
|
||||
// Extract the domain from entity_id (e.g., "light", "switch")
|
||||
const domain = entity_id.split(".")[0];
|
||||
|
||||
// Fetch services for the specific domain
|
||||
const valid_services = await getServicesForDomain(domain);
|
||||
|
||||
// Return device state and valid services
|
||||
return {
|
||||
entity_id: device.entity_id,
|
||||
state: device.state,
|
||||
friendly_name: device.attributes.friendly_name || "",
|
||||
valid_services, // Return valid services for this device
|
||||
attributes: {
|
||||
valid_states: device.attributes.valid_states || [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching state for device ${entity_id}:`, error);
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tools export
|
||||
export let homeAssistantTools: RunnableToolFunctionWithParse<any>[] = [
|
||||
// zodFunction({
|
||||
// function: getAllDevices,
|
||||
// name: "homeAssistantGetAllDevices",
|
||||
// schema: z.object({}), // No parameters needed
|
||||
// description:
|
||||
// "Get a list of all devices with their current states and valid services that can be called.",
|
||||
// }),
|
||||
zodFunction({
|
||||
function: setDeviceState,
|
||||
name: "homeAssistantSetDeviceState",
|
||||
schema: SetDeviceStateParams,
|
||||
description: `Set the state of a specific device by calling a valid service, such as 'turn_on' or 'turn_off'.
|
||||
|
||||
For simple text fields u can just use the following format too:
|
||||
|
||||
`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: fuzzySearchDevices,
|
||||
name: "homeAssistantFuzzySearchDevices",
|
||||
schema: FuzzySearchParams,
|
||||
description:
|
||||
"Search devices by name and return their entity_id, current state, and valid services that can be called to control the device.",
|
||||
}),
|
||||
];
|
||||
|
||||
export const HomeManagerParams = z.object({
|
||||
request: z.string().describe("What the user wants to do with which device"),
|
||||
// device_name: z.string().describe("What the user referred to the device as"),
|
||||
devices: z
|
||||
.array(z.string())
|
||||
.describe("The vague device names to potentially take action on"),
|
||||
});
|
||||
export type HomeManagerParams = z.infer<typeof HomeManagerParams>;
|
||||
|
||||
export async function homeManager(
|
||||
{ request, devices }: HomeManagerParams,
|
||||
context_message: Message
|
||||
) {
|
||||
const allMatches = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const { matches } = await fuzzySearchDevices({ query: device });
|
||||
if (matches?.length) {
|
||||
allMatches.push(...matches);
|
||||
}
|
||||
}
|
||||
|
||||
if (allMatches.length === 0) {
|
||||
return {
|
||||
error: `No devices found matching the provided names. Please try again.`,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `You are a home assistant manager.
|
||||
|
||||
----
|
||||
${memory_manager_guide("homeassistant-manager")}
|
||||
----
|
||||
|
||||
Similar devices were found based on the names provided:
|
||||
${JSON.stringify(allMatches)}
|
||||
|
||||
These are the devices that they may actually be referring to:
|
||||
${JSON.stringify(allMatches)}
|
||||
|
||||
Read the request carefully and perform the necessary action on only the RELEVANT devices.
|
||||
`,
|
||||
message: request,
|
||||
seed: `home-${context_message.channelId}`,
|
||||
tools: [
|
||||
...homeAssistantTools,
|
||||
memory_manager_init(context_message, "homeassistant-manager"),
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
response: response.choices[0].message.content,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,511 @@
|
|||
import {
|
||||
RunnableToolFunction,
|
||||
RunnableToolFunctionWithParse,
|
||||
} from "openai/lib/RunnableFunction.mjs";
|
||||
import { JSONSchema } from "openai/lib/jsonschema.mjs";
|
||||
import { z, ZodSchema } from "zod";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { evaluate } from "mathjs";
|
||||
|
||||
import {
|
||||
YoutubeDownloaderParams,
|
||||
YoutubeTranscriptParams,
|
||||
get_download_link,
|
||||
get_youtube_video_data,
|
||||
} from "./youtube";
|
||||
import {
|
||||
SendGeneralMessageParams,
|
||||
SendMessageParams,
|
||||
send_general_message,
|
||||
send_message_to,
|
||||
} from "./messenger";
|
||||
import {
|
||||
ChartParams,
|
||||
PythonCodeParams,
|
||||
RunPythonCommandParams,
|
||||
chart,
|
||||
code_interpreter,
|
||||
run_command_in_code_interpreter_env,
|
||||
} from "./python-interpreter";
|
||||
import { meme_maker, MemeMakerParams } from "./meme-maker";
|
||||
import { getPeriodTools } from "./period";
|
||||
import { linkManager, LinkManagerParams } from "./linkwarden";
|
||||
import { search_chat, SearchChatParams } from "./chat-search";
|
||||
import { getTotalCompletionTokensForModel } from "../usage";
|
||||
import {
|
||||
scrape_and_convert_to_markdown,
|
||||
ScrapeAndConvertToMarkdownParams,
|
||||
} from "./scrape";
|
||||
import { calendarManager, CalendarManagerParams } from "./calender";
|
||||
import { remindersManager, RemindersManagerParams } from "./reminders";
|
||||
import { notesManager, NotesManagerParams, webdav_tools } from "./notes";
|
||||
import { service_checker, ServiceCheckerParams } from "./status";
|
||||
import {
|
||||
upload_file,
|
||||
UploadFileParams,
|
||||
get_file_list,
|
||||
GetFileListParams,
|
||||
} from "./files";
|
||||
// Removed import of createContextMessage since it's not used here
|
||||
import { Message } from "../interfaces/message";
|
||||
import { rolePermissions, userConfigs } from "../config"; // <-- Added import
|
||||
import { search_user, SearchUserParams } from "./search-user";
|
||||
import { ResendParams, send_email } from "./resend";
|
||||
import { homeManager, HomeManagerParams } from "./home";
|
||||
import { event_manager, EventManagerSchema } from "./events";
|
||||
import { actionManager, ActionManagerParamsSchema } from "./actions";
|
||||
import { search_whatsapp_contacts, SearchContactsParams } from "./whatsapp";
|
||||
import { memory_manager_init } from "./memory-manager";
|
||||
import { communication_manager_tool } from "./communication";
|
||||
import { send_sys_log } from "../interfaces/log";
|
||||
|
||||
// get time function
|
||||
const GetTimeParams = z.object({});
|
||||
type GetTimeParams = z.infer<typeof GetTimeParams>;
|
||||
async function get_date_time({}: GetTimeParams) {
|
||||
return { response: new Date().toLocaleString() };
|
||||
}
|
||||
|
||||
// calculator function
|
||||
const CalculatorParams = z.object({
|
||||
expression: z.string().describe("mathjs expression"),
|
||||
});
|
||||
type CalculatorParams = z.infer<typeof CalculatorParams>;
|
||||
async function calculator({ expression }: CalculatorParams) {
|
||||
return { response: evaluate(expression) };
|
||||
}
|
||||
|
||||
// run bash command function and return all output success/errors both
|
||||
const RunBashCommandParams = z.object({
|
||||
command: z.string(),
|
||||
});
|
||||
type RunBashCommandParams = z.infer<typeof RunBashCommandParams>;
|
||||
async function run_bash_command({ command }: RunBashCommandParams) {
|
||||
console.log("running command: " + command);
|
||||
const { exec } = await import("child_process");
|
||||
return (await new Promise((resolve) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
console.log("stdout: " + stdout);
|
||||
console.log("stderr: " + stderr);
|
||||
if (error !== null) {
|
||||
console.log("exec error: " + error);
|
||||
}
|
||||
resolve({ stdout, stderr, error });
|
||||
});
|
||||
})) as { stdout: string; stderr: string; error: any };
|
||||
}
|
||||
|
||||
// exit process
|
||||
const ExitProcessParams = z.object({});
|
||||
type ExitProcessParams = z.infer<typeof ExitProcessParams>;
|
||||
async function restart_self({}: ExitProcessParams, context_message: Message) {
|
||||
await Promise.all([
|
||||
send_sys_log("Restarting myself"),
|
||||
context_message.send({
|
||||
content: "Restarting myself",
|
||||
}),
|
||||
context_message.send({
|
||||
content: "---setting this point as the start---",
|
||||
}),
|
||||
]);
|
||||
return { response: process.exit(0) };
|
||||
}
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// get total tokens used by a model
|
||||
const GetTotalTokensParams = z.object({
|
||||
model: z.string(),
|
||||
from: z.string(),
|
||||
to: z.string(),
|
||||
});
|
||||
|
||||
type GetTotalTokensParams = z.infer<typeof GetTotalTokensParams>;
|
||||
|
||||
async function get_total_tokens({ model, from, to }: GetTotalTokensParams) {
|
||||
return {
|
||||
response: getTotalCompletionTokensForModel(model, from, to),
|
||||
};
|
||||
}
|
||||
|
||||
export function getTools(
|
||||
username: string,
|
||||
context_message: Message,
|
||||
manager_id?: string
|
||||
) {
|
||||
const userRoles = context_message.getUserRoles();
|
||||
|
||||
// Aggregate permissions from all roles
|
||||
const userPermissions = new Set<string>();
|
||||
userRoles.forEach((role) => {
|
||||
const permissions = rolePermissions[role];
|
||||
if (permissions) {
|
||||
permissions.forEach((perm) => userPermissions.add(perm));
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to check if the user has access to a tool
|
||||
function hasAccess(toolName: string): boolean {
|
||||
if (toolName === "periodTools") {
|
||||
return userPermissions.has("periodUser");
|
||||
}
|
||||
return userPermissions.has("*") || userPermissions.has(toolName);
|
||||
}
|
||||
|
||||
// Define all tools with their names
|
||||
const allTools: {
|
||||
name: string;
|
||||
tool: RunnableToolFunction<any> | RunnableToolFunction<any>[];
|
||||
}[] = [
|
||||
{
|
||||
name: "calculator",
|
||||
tool: zodFunction({
|
||||
function: calculator,
|
||||
schema: CalculatorParams,
|
||||
description: "Evaluate math expression",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "getTime",
|
||||
tool: zodFunction({
|
||||
function: get_date_time,
|
||||
schema: GetTimeParams,
|
||||
description: "Get current date and time",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "search_user_ids",
|
||||
tool: zodFunction({
|
||||
function: (args) => search_user(args, context_message),
|
||||
name: "search_user_ids",
|
||||
schema: SearchUserParams,
|
||||
description: `Search and get user's details. Use this only when required.`,
|
||||
}),
|
||||
},
|
||||
|
||||
// Computer nerd tools
|
||||
|
||||
// {
|
||||
// name: "search_whatsapp_contacts",
|
||||
// tool: zodFunction({
|
||||
// function: search_whatsapp_contacts,
|
||||
// schema: SearchContactsParams,
|
||||
// description: `Search for contacts in user's whatsapp account. Use this to get whatsapp user_id of any user.
|
||||
// Note: Confirm from the user before sending any messages to the contacts found using this search.
|
||||
// `,
|
||||
// }),
|
||||
// },
|
||||
/* {
|
||||
name: "scrapeWeb",
|
||||
tool: zodFunction({
|
||||
function: scrape_and_convert_to_markdown,
|
||||
schema: ScrapeAndConvertToMarkdownParams,
|
||||
name: "scrape_web",
|
||||
description: `Get data from a webpage.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "uploadFile",
|
||||
tool: zodFunction({
|
||||
function: upload_file,
|
||||
schema: UploadFileParams,
|
||||
description: `Upload a LOCAL file to a MinIO bucket and return its public URL.
|
||||
|
||||
Note:
|
||||
- The filePath should be a local file path in the /tmp directory.
|
||||
- If you want to re-upload a file from the internet, you can download it using run_shell_command to a /tmp directory and then upload it.
|
||||
|
||||
Use cases:
|
||||
- You can use this to share files from inside the code interpreter using the /tmp file path.
|
||||
- You can use this to share files that only you have access to, like temporary files or discord files.
|
||||
- You can use this when the user explicitly asks for a file to be shared with them or wants to download a file.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "getFileList",
|
||||
tool: zodFunction({
|
||||
function: get_file_list,
|
||||
schema: GetFileListParams,
|
||||
description: `Get the list of public URLs for all files in the MinIO bucket`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "getYouTubeVideoData",
|
||||
tool: zodFunction({
|
||||
function: get_youtube_video_data,
|
||||
schema: YoutubeTranscriptParams as any,
|
||||
description:
|
||||
"Get YouTube video data. Use this only when sent a YouTube URL. Do not use this for YouTube search.",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "getDownloadLink",
|
||||
tool: zodFunction({
|
||||
function: get_download_link as any,
|
||||
schema: YoutubeDownloaderParams,
|
||||
description: `Get download link for YouTube links.
|
||||
Also, always hide the length of links that are too long by formatting them with markdown.
|
||||
For any site other than YouTube, use code interpreter to scrape the download link.
|
||||
|
||||
If the user wants the file and not just the link:
|
||||
You can use the direct link you get from this to download the media inside code interpreter and then share the downloaded files using the send message tool.
|
||||
Make sure that the file size is within discord limits.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "codeInterpreter",
|
||||
tool: zodFunction({
|
||||
function: (args) => code_interpreter(args, context_message),
|
||||
name: "code_interpreter",
|
||||
schema: PythonCodeParams,
|
||||
description: `Primary Function: Run Python code in an isolated environment.
|
||||
Key Libraries: pandas for data analysis, matplotlib for visualization.
|
||||
Use Cases: Data analysis, plotting, image/video processing using ffmpeg for video, complex calculations, and attachment analysis.
|
||||
You can also use this to try to scrape and get download links from non-YouTube sites.
|
||||
|
||||
File sharing:
|
||||
To share a file with a user from inside code interpreter, you can save the file to the /tmp/ directory and then use the send message tool to send the file to the user by using the full path of the file, including the /tmp part in the path.
|
||||
|
||||
Notes:
|
||||
Import necessary libraries; retry if issues arise.
|
||||
For web scraping, process data to stay within a 10,000 token limit.
|
||||
Use run_shell_command to check or install dependencies.
|
||||
Try to fix any errors that are returned at least once before sending to the user, especially syntax/type errors.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "runShellCommand",
|
||||
tool: zodFunction({
|
||||
function: (args) =>
|
||||
run_command_in_code_interpreter_env(args, context_message),
|
||||
name: "run_shell_command",
|
||||
schema: RunPythonCommandParams,
|
||||
description: `Run bash command. Use this to install any needed dependencies.`,
|
||||
}),
|
||||
}, */
|
||||
|
||||
// {
|
||||
// name: "generateChart",
|
||||
// tool: zodFunction({
|
||||
// function: chart,
|
||||
// name: "generate_chart",
|
||||
// schema: ChartParams,
|
||||
// description: `Generate chart PNG image URL using quickchart.io`,
|
||||
// }),
|
||||
// },
|
||||
// {
|
||||
// name: "memeOrCatMaker",
|
||||
// tool: zodFunction({
|
||||
// function: meme_maker,
|
||||
// name: "meme_or_cat_maker",
|
||||
// schema: MemeMakerParams,
|
||||
// description: `Generate meme image URL using memegen.link OR generate cat image URL using cataas.com
|
||||
|
||||
// Just provide the info in the query, and it will generate the URL for you.
|
||||
// This can include any memegen.link or cataas.com specific parameters.
|
||||
// Make sure to give as many details as you can about what the user wants.
|
||||
// Also, make sure to send the images and memes as files to the user using the send message tool unless explicitly asked to send the URL.`,
|
||||
// }),
|
||||
// },
|
||||
// {
|
||||
// name: "sendMessageToChannel",
|
||||
// tool: zodFunction({
|
||||
// function: (args) => send_general_message(args, context_message),
|
||||
// name: "send_message_to_channel",
|
||||
// schema: SendGeneralMessageParams,
|
||||
// description: `Send message to the current Discord channel.
|
||||
// You can also use this for reminders or other scheduled messages by calculating the delay from the current time.
|
||||
// If the user does not specify a time for a reminder, think of one based on the task.
|
||||
// If no channel ID is provided, the message will be sent to the user you are currently chatting with.`,
|
||||
// }),
|
||||
// },
|
||||
// {
|
||||
// name: "searchChat",
|
||||
// tool: zodFunction({
|
||||
// function: (args) => search_chat(args, context_message),
|
||||
// name: "search_chat",
|
||||
// schema: SearchChatParams,
|
||||
// description: `Search for messages in the current channel based on query parameters.
|
||||
// This will search the last 100 (configurable by setting the limit parameter) messages in the channel.
|
||||
// Set user_only parameter to true if you want to search only the user's messages.`,
|
||||
// }),
|
||||
// },
|
||||
{
|
||||
name: "serviceChecker",
|
||||
tool: zodFunction({
|
||||
function: service_checker,
|
||||
name: "service_checker",
|
||||
schema: ServiceCheckerParams,
|
||||
description: `Check the status of a service by querying the status page of the service. Use this when the user asks if something is up or down in the context of a service.`,
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// name: "getTotalTokens",
|
||||
// tool: zodFunction({
|
||||
// function: get_total_tokens,
|
||||
// name: "get_total_tokens",
|
||||
// schema: GetTotalTokensParams,
|
||||
// description: `Get total tokens used by a model in a date range
|
||||
|
||||
// The pricing as of 2024 is:
|
||||
// gpt-4o:
|
||||
// $5.00 / 1M prompt tokens
|
||||
// $15.00 / 1M completion tokens
|
||||
|
||||
// gpt-4o-mini:
|
||||
// $0.150 / 1M prompt tokens
|
||||
// $0.600 / 1M completion tokens
|
||||
|
||||
// Use calculator to make the math calculations.`,
|
||||
// }),
|
||||
// },
|
||||
{
|
||||
name: "communicationsManagerTool",
|
||||
tool: communication_manager_tool(context_message),
|
||||
},
|
||||
{
|
||||
name: "calendarManagerTool",
|
||||
tool: zodFunction({
|
||||
function: (args) => calendarManager(args, context_message),
|
||||
name: "calendar_manager",
|
||||
schema: CalendarManagerParams,
|
||||
description: `Manage calendar events using user's Calendar.
|
||||
You can just forward the user's request to this tool and it will handle the rest.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "remindersManagerTools",
|
||||
tool: zodFunction({
|
||||
function: (args) => remindersManager(args, context_message),
|
||||
name: "reminders_manager",
|
||||
schema: RemindersManagerParams,
|
||||
description: `Manage reminders using user's reminders.
|
||||
You can just forward the user's request to this tool and it will handle the rest.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "homeAssistantManagerTool",
|
||||
tool: zodFunction({
|
||||
function: (args) => homeManager(args, context_message),
|
||||
name: "home_assistant_manager",
|
||||
schema: HomeManagerParams,
|
||||
description: `Manage home assistant devices and services in natural language.
|
||||
Give as much details as possible to get the best results.
|
||||
Especially what devices that the user named and what action they want to perform on them.
|
||||
`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "notesManagerTool",
|
||||
tool: zodFunction({
|
||||
function: (args) => notesManager(args, context_message),
|
||||
name: "notes_manager",
|
||||
schema: NotesManagerParams,
|
||||
description: `Manage notes using user's notes.
|
||||
|
||||
You can just forward the user's request verbatim (or by adding more clarity) to this tool and it will handle the rest.
|
||||
|
||||
When to use:
|
||||
if user talks about any notes, lists, journal, gym entry, standup, personal journal, etc.
|
||||
`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "linkManagerTool",
|
||||
tool: zodFunction({
|
||||
function: (args) => linkManager(args, context_message),
|
||||
name: "link_manager",
|
||||
schema: LinkManagerParams,
|
||||
description: `Manage links using LinkWarden.
|
||||
You can just forward the user's request to this tool and it will handle the rest.
|
||||
`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "actionsManagerTool",
|
||||
tool: zodFunction({
|
||||
function: (args) => actionManager(args, context_message),
|
||||
name: "actions_manager",
|
||||
schema: ActionManagerParamsSchema,
|
||||
description: `Manage scheduled actions using the Actions Manager.
|
||||
|
||||
Forward user requests to create, update, retrieve, or remove actions.
|
||||
|
||||
You can use this for when a user wants you to do something at a specific time or after a specific time.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "eventsManagerTool",
|
||||
tool: zodFunction({
|
||||
function: (args) => event_manager(args, context_message),
|
||||
name: "events_manager",
|
||||
schema: EventManagerSchema,
|
||||
description: `Manage events using the Events Manager.
|
||||
|
||||
Forward user requests to create, update, retrieve, or remove events.
|
||||
|
||||
When to use:
|
||||
if user wants to create some automation based on some event.`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "restart",
|
||||
tool: zodFunction({
|
||||
function: (args) => restart_self(args, context_message),
|
||||
name: "restart_self",
|
||||
schema: ExitProcessParams,
|
||||
description:
|
||||
"Restart yourself. do this only when the user explicitly asks you to restart yourself.",
|
||||
}),
|
||||
},
|
||||
// {
|
||||
// name: "eventTools",
|
||||
// tool: event_tools(context_message),
|
||||
// },
|
||||
// Period tools
|
||||
{
|
||||
name: "periodTools",
|
||||
tool: getPeriodTools(context_message),
|
||||
},
|
||||
];
|
||||
|
||||
const manager_tools = manager_id
|
||||
? [memory_manager_init(context_message, manager_id)]
|
||||
: [];
|
||||
|
||||
// Filter tools based on user permissions
|
||||
const tools = allTools
|
||||
.filter(({ name }) => hasAccess(name))
|
||||
.flatMap(({ tool }) => (Array.isArray(tool) ? tool : [tool]))
|
||||
.concat(manager_tools);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
export function zodFunction<T extends object>({
|
||||
function: fn,
|
||||
schema,
|
||||
description = "",
|
||||
name,
|
||||
}: {
|
||||
function: (args: T) => Promise<object> | object;
|
||||
schema: ZodSchema<T>;
|
||||
description?: string;
|
||||
name?: string;
|
||||
}): RunnableToolFunctionWithParse<T> {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
function: fn,
|
||||
name: name ?? fn.name,
|
||||
description: description,
|
||||
parameters: zodToJsonSchema(schema) as JSONSchema,
|
||||
parse(input: string): T {
|
||||
const obj = JSON.parse(input);
|
||||
return schema.parse(obj);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,426 @@
|
|||
import axios from "axios";
|
||||
import { z } from "zod";
|
||||
import { zodFunction } from ".";
|
||||
import { RunnableToolFunction } from "openai/lib/RunnableFunction.mjs";
|
||||
import { ask } from "./ask";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: "https://link.raj.how/api/v1",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${process.env.LINKWARDEN_API_KEY}`,
|
||||
},
|
||||
});
|
||||
|
||||
// Schemas for each function's parameters
|
||||
export const ArchivedFileParams = z.object({
|
||||
id: z.string(),
|
||||
format: z.string().optional(),
|
||||
});
|
||||
export type ArchivedFileParams = z.infer<typeof ArchivedFileParams>;
|
||||
|
||||
export const TagParams = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
export type TagParams = z.infer<typeof TagParams>;
|
||||
|
||||
export const TagUpdateParams = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
color: z.string().optional(),
|
||||
});
|
||||
export type TagUpdateParams = z.infer<typeof TagUpdateParams>;
|
||||
|
||||
export const CollectionParams = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
export type CollectionParams = z.infer<typeof CollectionParams>;
|
||||
|
||||
export const CollectionLinksParams = z.object({
|
||||
collectionId: z.string(),
|
||||
tag: z.string().optional(),
|
||||
sortBy: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
});
|
||||
export type CollectionLinksParams = z.infer<typeof CollectionLinksParams>;
|
||||
|
||||
export const ProfileParams = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
export type ProfileParams = z.infer<typeof ProfileParams>;
|
||||
|
||||
export const MigrationParams = z.object({
|
||||
data: z.string(),
|
||||
});
|
||||
export type MigrationParams = z.infer<typeof MigrationParams>;
|
||||
|
||||
export const LinksParams = z.object({
|
||||
collectionId: z.string().optional(),
|
||||
tag: z.string().optional(),
|
||||
sortBy: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
});
|
||||
export type LinksParams = z.infer<typeof LinksParams>;
|
||||
|
||||
export const LinkParams = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
export type LinkParams = z.infer<typeof LinkParams>;
|
||||
|
||||
export const LinkUpdateParams = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
tagIds: z.array(z.string()).optional(),
|
||||
});
|
||||
export type LinkUpdateParams = z.infer<typeof LinkUpdateParams>;
|
||||
|
||||
export const ProfilePhotoParams = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
export type ProfilePhotoParams = z.infer<typeof ProfilePhotoParams>;
|
||||
|
||||
// Functions
|
||||
export async function getArchivedFile({ id, format }: ArchivedFileParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`/archives/${id}`, {
|
||||
params: { format },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllTags() {
|
||||
try {
|
||||
const response = await apiClient.get("/tags");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTag({ id, name, color }: TagUpdateParams) {
|
||||
try {
|
||||
const response = await apiClient.put(`/tags/${id}`, { name, color });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteTag({ id }: TagParams) {
|
||||
try {
|
||||
const response = await apiClient.delete(`/tags/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPublicCollectionInfo({ id }: CollectionParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`/public/collections/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLinksUnderPublicCollection({
|
||||
collectionId,
|
||||
tag,
|
||||
sortBy,
|
||||
limit,
|
||||
}: CollectionLinksParams) {
|
||||
try {
|
||||
const response = await apiClient.get("/public/collections/links", {
|
||||
params: { collectionId, tag, sortBy, limit },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSingleLinkUnderPublicCollection({ id }: LinkParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`/public/links/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPublicProfileInfo({ id }: ProfileParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`/public/users/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function importData({ data }: MigrationParams) {
|
||||
try {
|
||||
const response = await apiClient.post("/migration", { data });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportData({ data }: MigrationParams) {
|
||||
try {
|
||||
const response = await apiClient.get("/migration", { params: { data } });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLinks({
|
||||
collectionId,
|
||||
tag,
|
||||
sortBy,
|
||||
limit,
|
||||
}: LinksParams) {
|
||||
try {
|
||||
const response = await apiClient.get("/links", {
|
||||
params: { collectionId, tag, sortBy, limit },
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLink({ id }: LinkParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`/links/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Schema for adding a link
|
||||
export const AddLinkParams = z.object({
|
||||
url: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
tags: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
})
|
||||
),
|
||||
collection: z.object({
|
||||
id: z.number().default(8).optional(),
|
||||
}),
|
||||
});
|
||||
export type AddLinkParams = z.infer<typeof AddLinkParams>;
|
||||
|
||||
// Function to add a link
|
||||
export async function addLink(params: AddLinkParams) {
|
||||
try {
|
||||
const response = await apiClient.post("/links", params);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateLink({
|
||||
id,
|
||||
title,
|
||||
url,
|
||||
description,
|
||||
tagIds,
|
||||
}: LinkUpdateParams) {
|
||||
try {
|
||||
const response = await apiClient.put(`/links/${id}`, {
|
||||
title,
|
||||
url,
|
||||
description,
|
||||
tagIds,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteLink({ id }: LinkParams) {
|
||||
try {
|
||||
const response = await apiClient.delete(`/links/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function triggerArchiveForLink({ id }: LinkParams) {
|
||||
try {
|
||||
const response = await apiClient.put(`/links/${id}/archive`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDashboardData() {
|
||||
try {
|
||||
const response = await apiClient.get("/dashboard");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCollections() {
|
||||
try {
|
||||
const response = await apiClient.get("/collections");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProfilePhoto({ id }: ProfilePhotoParams) {
|
||||
try {
|
||||
const response = await apiClient.get(`/avatar/${id}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return `Error: ${error}`;
|
||||
}
|
||||
}
|
||||
|
||||
export let link_tools: RunnableToolFunction<any>[] = [
|
||||
zodFunction({
|
||||
function: getAllTags,
|
||||
name: "linkwardenGetAllTags",
|
||||
schema: z.object({}),
|
||||
description: "Get all tags for the user.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: updateTag,
|
||||
name: "linkwardenUpdateTag",
|
||||
schema: TagUpdateParams,
|
||||
description: "Update a tag.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: deleteTag,
|
||||
name: "linkwardenDeleteTag",
|
||||
schema: TagParams,
|
||||
description: "Delete a tag.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getPublicCollectionInfo,
|
||||
name: "linkwardenGetPublicCollectionInfo",
|
||||
schema: CollectionParams,
|
||||
description: "Get public collection info.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getLinksUnderPublicCollection,
|
||||
name: "linkwardenGetLinksUnderPublicCollection",
|
||||
schema: CollectionLinksParams,
|
||||
description: "Get links under a public collection.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getSingleLinkUnderPublicCollection,
|
||||
name: "linkwardenGetSingleLinkUnderPublicCollection",
|
||||
schema: LinkParams,
|
||||
description: "Get a single link under a public collection.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getPublicProfileInfo,
|
||||
name: "linkwardenGetPublicProfileInfo",
|
||||
schema: ProfileParams,
|
||||
description: "Get public profile info.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getLinks,
|
||||
name: "linkwardenGetLinks",
|
||||
schema: LinksParams,
|
||||
description: "Get links under a collection.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getLink,
|
||||
name: "linkwardenGetLink",
|
||||
schema: LinkParams,
|
||||
description: "Get a single link under a collection.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: updateLink,
|
||||
name: "linkwardenUpdateLink",
|
||||
schema: LinkUpdateParams,
|
||||
description: "Update a link.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: addLink,
|
||||
name: "linkwardenAddLink",
|
||||
schema: AddLinkParams,
|
||||
description:
|
||||
"Add a link (default to 'Anya' collection with id=8 if not specified).",
|
||||
}),
|
||||
zodFunction({
|
||||
function: deleteLink,
|
||||
name: "linkwardenDeleteLink",
|
||||
schema: LinkParams,
|
||||
description: "Delete a link.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: triggerArchiveForLink,
|
||||
name: "linkwardenTriggerArchiveForLink",
|
||||
schema: LinkParams,
|
||||
description: "Trigger archive for a link.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getDashboardData,
|
||||
name: "linkwardenGetDashboardData",
|
||||
schema: z.object({}),
|
||||
description: "Get dashboard data.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getCollections,
|
||||
name: "linkwardenGetCollections",
|
||||
schema: z.object({}),
|
||||
description: "Get all collections for the user.",
|
||||
}),
|
||||
];
|
||||
|
||||
export const LinkManagerParams = z.object({
|
||||
request: z
|
||||
.string()
|
||||
.describe("User's request regarding links, tags, or collections."),
|
||||
});
|
||||
export type LinkManagerParams = z.infer<typeof LinkManagerParams>;
|
||||
|
||||
export async function linkManager(
|
||||
{ request }: LinkManagerParams,
|
||||
context_message: Message
|
||||
) {
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `You are a Linkwarden manager.
|
||||
|
||||
Your job is to understand the user's request and manage links, tags, or collections using the available tools.
|
||||
|
||||
----
|
||||
${memory_manager_guide("links_manager")}
|
||||
----
|
||||
`,
|
||||
message: request,
|
||||
seed: "link-${context_message.channelId}",
|
||||
tools: link_tools.concat(
|
||||
memory_manager_init(context_message, "links_manager")
|
||||
) as any,
|
||||
});
|
||||
|
||||
return { response };
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,220 @@
|
|||
import { z } from "zod";
|
||||
import fs from "fs";
|
||||
import { randomUUID } from "crypto";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { zodFunction } from ".";
|
||||
import { ask } from "./ask";
|
||||
import { pathInDataDir } from "../config";
|
||||
|
||||
export const CreateMemorySchema = z.object({
|
||||
memory: z.string(),
|
||||
});
|
||||
|
||||
export type CreateMemory = z.infer<typeof CreateMemorySchema>;
|
||||
|
||||
export const UpdateMemorySchema = z.object({
|
||||
id: z.string(),
|
||||
memory: z.string(),
|
||||
});
|
||||
|
||||
export type UpdateMemory = z.infer<typeof UpdateMemorySchema>;
|
||||
|
||||
export const DeleteMemorySchema = z.object({
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export type DeleteMemory = z.infer<typeof DeleteMemorySchema>;
|
||||
|
||||
const memory_path = pathInDataDir("memories.json");
|
||||
|
||||
type Memories = Record<
|
||||
string,
|
||||
{
|
||||
id: string;
|
||||
memory: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}[]
|
||||
>;
|
||||
|
||||
// if the file doesn't exist, create it
|
||||
if (!fs.existsSync(memory_path)) {
|
||||
fs.writeFileSync(memory_path, "{}");
|
||||
}
|
||||
|
||||
function getMemories(): Memories {
|
||||
return JSON.parse(fs.readFileSync(memory_path, "utf-8"));
|
||||
}
|
||||
|
||||
export function getMemoriesByManager(manager_id: string) {
|
||||
return getMemories()[manager_id] || [];
|
||||
}
|
||||
|
||||
function saveMemories(memories: Memories) {
|
||||
fs.writeFileSync(memory_path, JSON.stringify(memories, null, 2));
|
||||
}
|
||||
|
||||
export function createMemory(params: CreateMemory, manager_id: string) {
|
||||
try {
|
||||
const memories = getMemories();
|
||||
memories[manager_id] = memories[manager_id] || [];
|
||||
if (memories[manager_id].length >= 5) {
|
||||
return { error: "You have reached the limit of memories." };
|
||||
}
|
||||
const uuid = randomUUID();
|
||||
const start = Math.floor(Math.random() * (uuid.length - 4));
|
||||
const new_mem = {
|
||||
id: uuid.slice(start, start + 4),
|
||||
memory: params.memory,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
memories[manager_id].push(new_mem);
|
||||
saveMemories(memories);
|
||||
return { id: new_mem.id };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
export function updateMemory(params: UpdateMemory, manager_id: string) {
|
||||
try {
|
||||
const memories = getMemories();
|
||||
memories[manager_id] = memories[manager_id] || [];
|
||||
const memory = memories[manager_id].find((m) => m.id === params.id);
|
||||
if (!memory) {
|
||||
return { error: "Memory not found" };
|
||||
}
|
||||
memory.memory = params.memory;
|
||||
memory.updated_at = new Date().toISOString();
|
||||
saveMemories(memories);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteMemory(params: DeleteMemory, manager_id: string) {
|
||||
try {
|
||||
const memories = getMemories();
|
||||
memories[manager_id] = memories[manager_id] || [];
|
||||
memories[manager_id] = memories[manager_id].filter(
|
||||
(m) => m.id !== params.id
|
||||
);
|
||||
saveMemories(memories);
|
||||
return {};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
export const memory_tools = (manager_id: string) => [
|
||||
zodFunction({
|
||||
function: (args) => createMemory(args, manager_id),
|
||||
name: "create_memory",
|
||||
schema: CreateMemorySchema,
|
||||
description: "Create a memory.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (args) => updateMemory(args, manager_id),
|
||||
name: "update_memory",
|
||||
schema: UpdateMemorySchema,
|
||||
description: "Update a memory.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: (args) => deleteMemory(args, manager_id),
|
||||
name: "delete_memory",
|
||||
schema: DeleteMemorySchema,
|
||||
description: "Delete a memory.",
|
||||
}),
|
||||
];
|
||||
|
||||
const MemoryManagerSchema = z.object({
|
||||
request: z.string(),
|
||||
});
|
||||
|
||||
export type MemoryManager = z.infer<typeof MemoryManagerSchema>;
|
||||
|
||||
async function memoryManager(
|
||||
params: MemoryManager,
|
||||
context_message: Message,
|
||||
manager_id: string
|
||||
) {
|
||||
try {
|
||||
const current_memories = getMemories()[manager_id] || [];
|
||||
const tools = memory_tools(manager_id);
|
||||
|
||||
const response = await ask({
|
||||
model: "gpt-4o",
|
||||
prompt: `You are a Memories Manager.
|
||||
|
||||
You manage memories for other managers.
|
||||
|
||||
Help the manager with their request based on the information provided.
|
||||
|
||||
- **Priority:** Only store useful and detailed memories.
|
||||
- If the request is not useful or lacks detail, ask for more information or deny the request.
|
||||
- When a manager reaches the memory limit, ask them to choose memories to delete. Ensure they inform their user about the deletion and confirm before proceeding.
|
||||
- Ensure the memories are relevant to the requesting manager.
|
||||
- Return the ID of any memory you save so the manager can refer to it later.
|
||||
|
||||
**Current Manager:** ${manager_id}
|
||||
|
||||
**Their Memories:**
|
||||
${JSON.stringify(current_memories)}
|
||||
`,
|
||||
message: params.request,
|
||||
name: manager_id,
|
||||
seed: `memory-man-${manager_id}-${
|
||||
context_message.author.username ?? context_message.author.id
|
||||
}`,
|
||||
tools,
|
||||
});
|
||||
|
||||
return {
|
||||
response: response.choices[0].message.content,
|
||||
};
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
export const memory_manager_init = (
|
||||
context_message: Message,
|
||||
manager_id: string
|
||||
) => {
|
||||
return zodFunction({
|
||||
function: (args) => memoryManager(args, context_message, manager_id),
|
||||
name: "memory_manager",
|
||||
schema: MemoryManagerSchema,
|
||||
description:
|
||||
`Manages memories for a manager or yourself.
|
||||
|
||||
- Memories are isolated per manager; managers can't access each other's memories.
|
||||
- **Use Cases:**
|
||||
- Remembering important user preferences.
|
||||
- Anything you want to recall later.
|
||||
|
||||
**Examples:**
|
||||
- "Remember to use \`home_assistant_manager\` when the user asks to set text on a widget."
|
||||
- "Remember that the user mainly cares about the P4 system service status."
|
||||
|
||||
Memories are limited and costly; use them wisely.
|
||||
` +
|
||||
manager_id ===
|
||||
"self"
|
||||
? `### Imporant Note
|
||||
Make sure you only use this for your own memories and not for other memories that you can tell other managers to remember.
|
||||
`
|
||||
: "",
|
||||
});
|
||||
};
|
||||
|
||||
export const memory_manager_guide = (
|
||||
manager_id: string
|
||||
) => `# Memories Saved for You
|
||||
|
||||
${JSON.stringify(getMemoriesByManager(manager_id), null, 2)}
|
||||
|
||||
You can store up to 5 memories at a time. Use them wisely.
|
||||
`;
|
|
@ -0,0 +1,244 @@
|
|||
// tools/tokio.ts
|
||||
|
||||
import { z } from "zod";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { userConfigs } from "../config";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { getMessageInterface } from "../interfaces";
|
||||
|
||||
// Utility function to get user ID by name and platform
|
||||
function getUserIdByName(
|
||||
userName: string,
|
||||
platform: string
|
||||
): string | undefined {
|
||||
const userConfig = userConfigs.find(
|
||||
(user) => user.name.toLowerCase() === userName.toLowerCase()
|
||||
);
|
||||
if (userConfig) {
|
||||
const identity = userConfig.identities.find(
|
||||
(id) => id.platform === platform
|
||||
);
|
||||
return identity?.id;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// SendMessageParams schema
|
||||
export const SendMessageParams = z.object({
|
||||
user_id: z
|
||||
.string()
|
||||
.describe(
|
||||
"The user id of the user specific to the platform selected. This will be different from the user's name and depends on the platform."
|
||||
),
|
||||
content: z
|
||||
.string()
|
||||
.describe(
|
||||
"The message to send to the user. Make sure to include the name of the person who asked you to send the message. You can only link HTTP URLs in links; you cannot link file paths EVER."
|
||||
)
|
||||
.optional(),
|
||||
platform: z.string().describe("The platform to send the message to."),
|
||||
embeds: z
|
||||
.array(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
color: z.number().optional(),
|
||||
timestamp: z.string().optional(),
|
||||
footer: z
|
||||
.object({
|
||||
text: z.string(),
|
||||
icon_url: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
image: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
thumbnail: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
"Embeds to send. Include the name of the person who asked you to send the message in the footer as the from user."
|
||||
),
|
||||
files: z
|
||||
.array(
|
||||
z.object({
|
||||
attachment: z.string().describe("URL/path to file to send"),
|
||||
name: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
export type SendMessageParams = z.infer<typeof SendMessageParams>;
|
||||
|
||||
// Function to send a message to a user
|
||||
export async function send_message_to(
|
||||
{ content, embeds, files, user_id, platform: plat }: SendMessageParams,
|
||||
context_message: Message
|
||||
) {
|
||||
if (!plat) {
|
||||
return { error: "Please specify a platform" };
|
||||
}
|
||||
|
||||
const platform = (plat || context_message.platform).toLocaleLowerCase();
|
||||
if (
|
||||
!context_message.getUserRoles().includes("admin") &&
|
||||
platform === "whatsapp"
|
||||
) {
|
||||
return {
|
||||
error:
|
||||
"You need to be Raj to send messages on WhatsApp, you are not allowed",
|
||||
};
|
||||
}
|
||||
|
||||
if (platform === "whatsapp") {
|
||||
const roles = context_message.getUserRoles();
|
||||
if (!roles.includes("admin")) {
|
||||
return { error: "User is not allowed to send messages on WhatsApp." };
|
||||
}
|
||||
}
|
||||
|
||||
// if platform is whatsapp and user_id does not end with @c.us, add it
|
||||
if (platform === "whatsapp" && user_id && !user_id.endsWith("@c.us")) {
|
||||
user_id = user_id + "@c.us";
|
||||
}
|
||||
// Get the recipient's user ID
|
||||
let toUserId: string | undefined = user_id;
|
||||
|
||||
try {
|
||||
// Prepare the message data
|
||||
const messageData = { content, embeds, files };
|
||||
if (user_id) {
|
||||
if (plat !== context_message.platform) {
|
||||
const local_ctx = await getMessageInterface({
|
||||
platform: plat || platform,
|
||||
id: user_id,
|
||||
});
|
||||
try {
|
||||
await local_ctx.send(messageData);
|
||||
return {
|
||||
response: "Message sent",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!toUserId) {
|
||||
return { error: "User not found on this platform." };
|
||||
}
|
||||
|
||||
if (!content && !embeds && !files) {
|
||||
return {
|
||||
error: "At least one of content, embeds, or files is required.",
|
||||
};
|
||||
}
|
||||
|
||||
await context_message.sendDirectMessage(toUserId, messageData);
|
||||
return {
|
||||
response:
|
||||
"Message sent, generate notification text telling that message was successfully sent.",
|
||||
note:
|
||||
toUserId === context_message.author.id
|
||||
? `The message was sent to the user. Reply to the user with "<NOREPLY>" as you already sent them a message.`
|
||||
: "The message was sent to the user. You can also tell what you sent the user if required.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { error: error.message || error };
|
||||
}
|
||||
}
|
||||
|
||||
// SendGeneralMessageParams schema
|
||||
export const SendGeneralMessageParams = z.object({
|
||||
channel_id: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Channel ID to send the message to."),
|
||||
content: z
|
||||
.string()
|
||||
.describe(
|
||||
"The message to send. Make sure to include the name of the person who asked you to send the message. You can only link HTTP URLs in links; you cannot link file paths EVER."
|
||||
)
|
||||
.optional(),
|
||||
embeds: z
|
||||
.array(
|
||||
z.object({
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
color: z.number().optional(),
|
||||
timestamp: z.string().optional(),
|
||||
footer: z
|
||||
.object({
|
||||
text: z.string(),
|
||||
icon_url: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
image: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
thumbnail: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.describe(
|
||||
"Embeds to send. Include the name of the person who asked you to send the message in the footer as the from user."
|
||||
),
|
||||
files: z
|
||||
.array(
|
||||
z.object({
|
||||
attachment: z.string().describe("URL/path to file to send"),
|
||||
name: z.string(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
export type SendGeneralMessageParams = z.infer<typeof SendGeneralMessageParams>;
|
||||
|
||||
// Function to send a message to a channel
|
||||
export async function send_general_message(
|
||||
{ channel_id, content, embeds, files }: SendGeneralMessageParams,
|
||||
context_message: Message
|
||||
) {
|
||||
const platform = context_message.platform;
|
||||
|
||||
// Use the provided channel ID or default to the current channel
|
||||
const targetChannelId = channel_id || context_message.channelId;
|
||||
|
||||
if (!targetChannelId) {
|
||||
return { error: "Channel ID is required." };
|
||||
}
|
||||
|
||||
if (!content && !embeds && !files) {
|
||||
return { error: "At least one of content, embeds, or files is required." };
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare the message data
|
||||
const messageData = { content, embeds, files };
|
||||
|
||||
await context_message.sendMessageToChannel(targetChannelId, messageData);
|
||||
return {
|
||||
response: "Message sent",
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { error: error.message || error };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import metaFetcher from "meta-fetcher";
|
||||
import { z } from "zod";
|
||||
|
||||
// get meta data from url
|
||||
export const MetaFetcherParams = z.object({
|
||||
url: z.string(),
|
||||
});
|
||||
export type MetaFetcherParams = z.infer<typeof MetaFetcherParams>;
|
||||
export async function meta_fetcher({ url }: MetaFetcherParams) {
|
||||
return await metaFetcher(url);
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
import { createClient, ResponseDataDetailed } from "webdav";
|
||||
import { z } from "zod";
|
||||
import { zodFunction } from ".";
|
||||
import { RunnableToolFunction } from "openai/lib/RunnableFunction.mjs";
|
||||
import Fuse from "fuse.js";
|
||||
import { ask } from "./ask";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
|
||||
// Initialize WebDAV client
|
||||
const client = createClient("http://192.168.29.85/remote.php/dav/files/raj/", {
|
||||
username: process.env.NEXTCLOUD_USERNAME!,
|
||||
password: process.env.NEXTCLOUD_PASSWORD!,
|
||||
});
|
||||
|
||||
// Types
|
||||
export type OperationResult = { success: boolean; message: string | object };
|
||||
// Schemas for function parameters
|
||||
export const CreateFileParams = z.object({
|
||||
path: z.string().describe("The path for the new file."),
|
||||
content: z.string().describe("The content for the new file."),
|
||||
});
|
||||
export type CreateFileParams = z.infer<typeof CreateFileParams>;
|
||||
|
||||
export const CreateDirectoryParams = z.object({
|
||||
path: z.string().describe("The path for the new directory."),
|
||||
});
|
||||
export type CreateDirectoryParams = z.infer<typeof CreateDirectoryParams>;
|
||||
|
||||
export const DeleteItemParams = z.object({
|
||||
path: z.string().describe("The path to the file or directory to be deleted."),
|
||||
});
|
||||
export type DeleteItemParams = z.infer<typeof DeleteItemParams>;
|
||||
|
||||
export const MoveItemParams = z.object({
|
||||
source_path: z
|
||||
.string()
|
||||
.describe("The current path of the file or directory."),
|
||||
destination_path: z
|
||||
.string()
|
||||
.describe("The new path where the file or directory will be moved."),
|
||||
});
|
||||
export type MoveItemParams = z.infer<typeof MoveItemParams>;
|
||||
|
||||
export const SearchFilesParams = z.object({
|
||||
query: z
|
||||
.string()
|
||||
.describe("The query string to search for file names or content."),
|
||||
});
|
||||
export type SearchFilesParams = z.infer<typeof SearchFilesParams>;
|
||||
|
||||
export const TagParams = z.object({
|
||||
path: z.string().describe("The path to the file to tag."),
|
||||
tag: z.string().describe("The tag to add to the file."),
|
||||
});
|
||||
export type TagParams = z.infer<typeof TagParams>;
|
||||
|
||||
// Helper function to remove the "notes/" prefix
|
||||
function normalizePath(path: string): string {
|
||||
return path.startsWith("notes/") ? path.substring(6) : path;
|
||||
}
|
||||
|
||||
// Function to create a file
|
||||
export async function createFile({
|
||||
path,
|
||||
content,
|
||||
}: CreateFileParams): Promise<OperationResult> {
|
||||
try {
|
||||
await client.putFileContents(`/notes/${normalizePath(path)}`, content);
|
||||
return { success: true, message: "File created successfully" };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to create a directory
|
||||
export async function createDirectory({
|
||||
path,
|
||||
}: CreateDirectoryParams): Promise<OperationResult> {
|
||||
try {
|
||||
await client.createDirectory(`/notes/${normalizePath(path)}`);
|
||||
return { success: true, message: "Directory created successfully" };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to delete a file or directory
|
||||
export async function deleteItem({
|
||||
path,
|
||||
}: DeleteItemParams): Promise<OperationResult> {
|
||||
try {
|
||||
await client.deleteFile(`/notes/${normalizePath(path)}`);
|
||||
return { success: true, message: "Deleted successfully" };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to move a file or directory
|
||||
export async function moveItem({
|
||||
source_path,
|
||||
destination_path,
|
||||
}: MoveItemParams): Promise<OperationResult> {
|
||||
try {
|
||||
await client.moveFile(
|
||||
`/notes/${normalizePath(source_path)}`,
|
||||
`/notes/${normalizePath(destination_path)}`
|
||||
);
|
||||
return { success: true, message: "Moved successfully" };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to search for files by name
|
||||
export async function searchFilesByName({
|
||||
query,
|
||||
}: SearchFilesParams): Promise<OperationResult> {
|
||||
try {
|
||||
const files = await client.getDirectoryContents("notes", {
|
||||
details: true,
|
||||
deep: true,
|
||||
});
|
||||
|
||||
// If `files` is of type `ResponseDataDetailed<FileStat[]>`, you need to access the data property
|
||||
const fileList = Array.isArray(files) ? files : files.data;
|
||||
|
||||
// Setup fuse.js with the filenames
|
||||
const fuse = new Fuse(fileList, {
|
||||
keys: ["filename"], // Search within filenames
|
||||
threshold: 0.3, // Adjust this to control the fuzziness (0 = exact match, 1 = very fuzzy)
|
||||
});
|
||||
|
||||
const matchingFiles = fuse.search(query).map((result) => result.item);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message:
|
||||
matchingFiles.length > 0
|
||||
? matchingFiles.map((file) => file.filename).join(", ")
|
||||
: "No matching files found",
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Function to search for files by content
|
||||
export async function searchFilesByContent({
|
||||
query,
|
||||
}: SearchFilesParams): Promise<OperationResult> {
|
||||
try {
|
||||
const files = await client.getDirectoryContents("notes", {
|
||||
details: true,
|
||||
deep: true,
|
||||
});
|
||||
|
||||
// If `files` is of type `ResponseDataDetailed<FileStat[]>`, you need to access the data property
|
||||
const fileList = Array.isArray(files) ? files : files.data;
|
||||
|
||||
// First, filter files by filename using fuse.js
|
||||
const fuseFilename = new Fuse(fileList, {
|
||||
keys: ["basename"], // Search within filenames
|
||||
threshold: 0.3, // Adjust this to control the fuzziness
|
||||
});
|
||||
const matchingFilesByName = fuseFilename
|
||||
.search(query)
|
||||
.map((result) => result.item);
|
||||
|
||||
const matchingFilesByContent = [];
|
||||
|
||||
// Then, check file content
|
||||
for (const file of fileList) {
|
||||
if (file.type === "file") {
|
||||
const content = await client.getFileContents(file.filename, {
|
||||
format: "text",
|
||||
});
|
||||
const fuseContent = new Fuse([String(content)], {
|
||||
threshold: 0.3, // Adjust for content search
|
||||
});
|
||||
const contentMatch = fuseContent.search(query);
|
||||
if (contentMatch.length > 0) {
|
||||
matchingFilesByContent.push(normalizePath(file.filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combine results from filename and content search
|
||||
const combinedResults = [
|
||||
...new Set([
|
||||
...matchingFilesByName.map((f) => f.filename),
|
||||
...matchingFilesByContent,
|
||||
]),
|
||||
];
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message:
|
||||
combinedResults.length > 0
|
||||
? combinedResults.join(", ")
|
||||
: "No matching files found",
|
||||
};
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder for tagging functionality
|
||||
export async function tagFile({
|
||||
path,
|
||||
tag,
|
||||
}: TagParams): Promise<OperationResult> {
|
||||
return { success: false, message: "Tagging not supported with WebDAV." };
|
||||
}
|
||||
|
||||
// Placeholder for searching files by tag
|
||||
export async function searchFilesByTag({
|
||||
tag,
|
||||
}: TagParams): Promise<OperationResult> {
|
||||
return { success: false, message: "Tagging not supported with WebDAV." };
|
||||
}
|
||||
|
||||
export async function getNotesList(): Promise<OperationResult> {
|
||||
try {
|
||||
const directoryContents = await fetchDirectoryContents("notes");
|
||||
|
||||
const treeStructure = buildTree(directoryContents as any);
|
||||
return {
|
||||
success: true,
|
||||
message: JSON.stringify(treeStructure, null, 2),
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDirectoryContents(
|
||||
path: string
|
||||
): Promise<ReturnType<typeof client.getDirectoryContents>> {
|
||||
let contents = await client.getDirectoryContents(path);
|
||||
|
||||
// Normalize contents to always be an array of FileStat
|
||||
if (!Array.isArray(contents)) {
|
||||
contents = contents.data; // Assuming it's ResponseDataDetailed<FileStat[]>
|
||||
}
|
||||
|
||||
// Recursively fetch the contents of subdirectories
|
||||
for (const item of contents) {
|
||||
if (item.type === "directory") {
|
||||
const subdirectoryContents = await fetchDirectoryContents(item.filename);
|
||||
contents = contents.concat(subdirectoryContents as any);
|
||||
}
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
function buildTree(files: any[]): any {
|
||||
const tree: any = {};
|
||||
|
||||
files.forEach((file) => {
|
||||
const parts: string[] = file.filename.replace(/^\/notes\//, "").split("/");
|
||||
let current = tree;
|
||||
|
||||
parts.forEach((part, index) => {
|
||||
if (!current[part]) {
|
||||
current[part] = index === parts.length - 1 ? null : {}; // Leaf nodes are set to null
|
||||
}
|
||||
current = current[part];
|
||||
});
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
export const FetchFileContentsParams = z.object({
|
||||
path: z
|
||||
.string()
|
||||
.describe("The path to the file whose content is to be fetched."),
|
||||
});
|
||||
export type FetchFileContentsParams = z.infer<typeof FetchFileContentsParams>;
|
||||
|
||||
// The fetchFileContents function
|
||||
export async function fetchFileContents({
|
||||
path,
|
||||
}: FetchFileContentsParams): Promise<OperationResult> {
|
||||
try {
|
||||
// Fetch the file content from the WebDAV server
|
||||
const fileContent: ResponseDataDetailed<string> =
|
||||
(await client.getFileContents(`/notes/${normalizePath(path)}`, {
|
||||
format: "text",
|
||||
details: true,
|
||||
})) as ResponseDataDetailed<string>;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: fileContent,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const UpdateFileParams = z.object({
|
||||
path: z.string().describe("The path to the note file to be updated."),
|
||||
new_content: z
|
||||
.string()
|
||||
.describe("The new content to replace the existing content."),
|
||||
});
|
||||
export type UpdateFileParams = z.infer<typeof UpdateFileParams>;
|
||||
|
||||
export async function updateNote({
|
||||
path,
|
||||
new_content,
|
||||
}: UpdateFileParams): Promise<OperationResult> {
|
||||
try {
|
||||
// Fetch the existing content to ensure the file exists and to avoid overwriting unintentionally
|
||||
const existingContent = await client.getFileContents(
|
||||
`/notes/${normalizePath(path)}`,
|
||||
{
|
||||
format: "text",
|
||||
}
|
||||
);
|
||||
|
||||
// Update the file with the new content
|
||||
await client.putFileContents(`/notes/${normalizePath(path)}`, new_content);
|
||||
|
||||
return { success: true, message: "Note updated successfully" };
|
||||
} catch (error: any) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Integration into runnable tools
|
||||
export let webdav_tools: RunnableToolFunction<any>[] = [
|
||||
zodFunction({
|
||||
function: getNotesList,
|
||||
name: "getNotesList",
|
||||
schema: z.object({}),
|
||||
description: "Get the list of note files and directories.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: fetchFileContents,
|
||||
name: "fetchNoteFileContents",
|
||||
schema: FetchFileContentsParams,
|
||||
description: "Fetch the contents of a specific note file.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: createFile,
|
||||
name: "createNoteFile",
|
||||
schema: CreateFileParams,
|
||||
description: "Create a new note file.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: updateNote,
|
||||
name: "updateNote",
|
||||
schema: UpdateFileParams,
|
||||
description: "Update an existing note.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: createDirectory,
|
||||
name: "createNoteDir",
|
||||
schema: CreateDirectoryParams,
|
||||
description: "Create a new directory in notes.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: deleteItem,
|
||||
name: "deleteNoteItem",
|
||||
schema: DeleteItemParams,
|
||||
description: "Delete a note file or directory.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: moveItem,
|
||||
name: "moveNote",
|
||||
schema: MoveItemParams,
|
||||
description: "Move a note file or directory.",
|
||||
}),
|
||||
// zodFunction({
|
||||
// function: searchFilesByName,
|
||||
// name: "searchNotesFilesByName",
|
||||
// schema: SearchFilesParams,
|
||||
// description: "Search notes by filename.",
|
||||
// }),
|
||||
zodFunction({
|
||||
function: searchFilesByContent,
|
||||
name: "searchNotesFilesByContent",
|
||||
schema: SearchFilesParams,
|
||||
description: "Search notes by content.",
|
||||
}),
|
||||
zodFunction({
|
||||
function: tagFile,
|
||||
name: "tagNoteFile",
|
||||
schema: TagParams,
|
||||
description: "Add a tag to a note file.",
|
||||
}),
|
||||
// zodFunction({
|
||||
// function: searchFilesByTag,
|
||||
// name: "searchNotesFilesByTag",
|
||||
// schema: TagParams,
|
||||
// description: "Search notes by tag.",
|
||||
// }),
|
||||
];
|
||||
|
||||
export function getNotesSystemPrompt() {
|
||||
return `The notes system manages a structured file system for organizing and retrieving notes using Nextcloud via WebDAV. All notes are stored in the 'notes' directory, with subdirectories for different content types.
|
||||
|
||||
**Key Directories:**
|
||||
|
||||
- **Root**: Contains a 'readme' summarizing the structure.
|
||||
- **Journal**: Logs daily events and activities. Subdirectories include:
|
||||
- **general**: General daily events or notes.
|
||||
- **standup**: Work-related standup notes. Filenames should be dates in YYYY-MM-DD format.
|
||||
- **personal**: Personal life events, same format as standup notes.
|
||||
- **gym**: Gym or workout activities.
|
||||
|
||||
- **Lists**: Contains lists of items or tasks. Subdirectories can organize different list types.
|
||||
|
||||
**Standup and Personal Note Template:**
|
||||
|
||||
- **Filename**: Date in YYYY-MM-DD format.
|
||||
- **Title**: Human-readable date (e.g., "Thursday 15th of July"), year not necessary.
|
||||
- **Updates Section**: List of updates describing the day's events.
|
||||
- **Summary Section**: A summary of the main points.
|
||||
|
||||
**Gym Note Template:**
|
||||
|
||||
- **Filename**: Date in YYYY-MM-DD format.
|
||||
- **Title**: Gym day and date (e.g., "Pull Day - Thursday 15th of July").
|
||||
- **Activity**: Exercises performed, sets, reps, weights.
|
||||
- **Progress Report**: Progress updates, achievements, challenges, comparisons with previous workouts, suggestions for improvement.
|
||||
|
||||
**Lists Template:**
|
||||
|
||||
- **Directory Structure**: Create subdirectories within 'lists' for different types (e.g., 'shows', 'movies', 'shopping').
|
||||
- **Filename**: Each file represents a list item with context. For 'shopping', use a single file like 'shopping.md'.
|
||||
|
||||
**Functionality:**
|
||||
|
||||
- Create, update, delete and move notes by filename or content.
|
||||
- The \`updateNote\` function modifies existing notes.
|
||||
|
||||
This system ensures efficient note management, avoiding duplication, maintaining organization, and following structured templates for work and personal notes.`;
|
||||
}
|
||||
|
||||
export const NotesManagerParams = z.object({
|
||||
request: z.string().describe("User's request regarding notes."),
|
||||
});
|
||||
export type NotesManagerParams = z.infer<typeof NotesManagerParams>;
|
||||
|
||||
export async function notesManager(
|
||||
{ request }: NotesManagerParams,
|
||||
context_message: Message
|
||||
) {
|
||||
const tools = webdav_tools.concat(
|
||||
memory_manager_init(context_message, "notes_manager")
|
||||
);
|
||||
const response = await ask({
|
||||
model: "gpt-4o",
|
||||
prompt: `You are a notes manager for the 'notes' directory in Nextcloud.
|
||||
|
||||
Your job is to understand the user's request (e.g., create, update, delete, move, list) and handle it using the available tools. Ensure the 'notes' directory remains organized, filenames and paths are correct, and duplication is prevented.
|
||||
|
||||
Avoid running \`fetchNoteFileContents\` unnecessarily, as it fetches the entire file content and is resource-intensive.
|
||||
|
||||
**More about the Notes System:**
|
||||
|
||||
${getNotesSystemPrompt()}
|
||||
|
||||
Follow the above guidelines to manage notes efficiently.
|
||||
|
||||
----
|
||||
|
||||
${memory_manager_guide("notes_manager")}
|
||||
|
||||
----
|
||||
|
||||
**Live Values:**
|
||||
|
||||
- **Today's Date:** ${new Date().toDateString()}
|
||||
- **Current Notes List:**
|
||||
${(await getNotesList()).message}
|
||||
`,
|
||||
message: request,
|
||||
seed: `notes-${context_message.channelId}`,
|
||||
tools: tools as any,
|
||||
});
|
||||
|
||||
return { response };
|
||||
}
|
|
@ -0,0 +1,715 @@
|
|||
// tools/period.ts
|
||||
|
||||
import { z, ZodSchema } from "zod";
|
||||
import { Database } from "bun:sqlite";
|
||||
import {
|
||||
RunnableToolFunction,
|
||||
RunnableToolFunctionWithParse,
|
||||
} from "openai/lib/RunnableFunction.mjs";
|
||||
import { JSONSchema } from "openai/lib/jsonschema.mjs";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { ask } from "./ask";
|
||||
import cron from "node-cron";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { pathInDataDir, userConfigs } from "../config";
|
||||
import { getMessageInterface } from "../interfaces";
|
||||
|
||||
// Populate example data function
|
||||
export function populateExampleData() {
|
||||
db.query("DELETE FROM period_cycles").run();
|
||||
db.query("DELETE FROM period_entries").run();
|
||||
}
|
||||
|
||||
export function clearprdandtestdb() {
|
||||
if (db) db.close();
|
||||
const prddb = usePrdDb();
|
||||
const testdb = useTestDb();
|
||||
prddb.query("DELETE FROM period_cycles").run();
|
||||
prddb.query("DELETE FROM period_entries").run();
|
||||
|
||||
testdb.query("DELETE FROM period_cycles").run();
|
||||
testdb.query("DELETE FROM period_entries").run();
|
||||
}
|
||||
|
||||
// Util functions for managing menstrual cycle
|
||||
|
||||
const PeriodCycleSchema = z.object({
|
||||
id: z.string(),
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
description: z.string(),
|
||||
ended: z.boolean(),
|
||||
});
|
||||
|
||||
const PeriodEntrySchema = z.object({
|
||||
id: z.string(),
|
||||
date: z.string(),
|
||||
description: z.string(),
|
||||
});
|
||||
|
||||
export type PeriodCycleType = z.infer<typeof PeriodCycleSchema>;
|
||||
export type PeriodEntryType = z.infer<typeof PeriodEntrySchema>;
|
||||
|
||||
let db = usePrdDb();
|
||||
|
||||
function usePrdDb() {
|
||||
const db_url = pathInDataDir("period.db");
|
||||
const db = new Database(db_url, { create: true });
|
||||
// Setup the tables
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
db.query(
|
||||
`CREATE TABLE IF NOT EXISTS period_cycles (
|
||||
id TEXT PRIMARY KEY,
|
||||
startDate TEXT NOT NULL,
|
||||
endDate TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
ended BOOLEAN NOT NULL
|
||||
)`
|
||||
).run();
|
||||
|
||||
db.query(
|
||||
`CREATE TABLE IF NOT EXISTS period_entries (
|
||||
id TEXT PRIMARY KEY,
|
||||
date TEXT NOT NULL,
|
||||
description TEXT NOT NULL
|
||||
)`
|
||||
).run();
|
||||
return db;
|
||||
}
|
||||
|
||||
function useTestDb() {
|
||||
const db_url = pathInDataDir("test_period.db");
|
||||
const db = new Database(db_url, { create: true });
|
||||
// Setup the tables
|
||||
db.exec("PRAGMA journal_mode = WAL;");
|
||||
db.query(
|
||||
`CREATE TABLE IF NOT EXISTS period_cycles (
|
||||
id TEXT PRIMARY KEY,
|
||||
startDate TEXT NOT NULL,
|
||||
endDate TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
ended BOOLEAN NOT NULL
|
||||
)`
|
||||
).run();
|
||||
|
||||
db.query(
|
||||
`CREATE TABLE IF NOT EXISTS period_entries (
|
||||
id TEXT PRIMARY KEY,
|
||||
date TEXT NOT NULL,
|
||||
description TEXT NOT NULL
|
||||
)`
|
||||
).run();
|
||||
return db;
|
||||
}
|
||||
|
||||
export function getPeriodCycles() {
|
||||
const cycles = db.query("SELECT * FROM period_cycles").all();
|
||||
return cycles as PeriodCycleType[];
|
||||
}
|
||||
|
||||
// Other utility functions remain the same...
|
||||
|
||||
export function getPeriodCyclesByMonth(month_index: number, year: number) {
|
||||
const startDate = new Date(year, month_index, 1).toISOString();
|
||||
const endDate = new Date(year, month_index + 1, 1).toISOString();
|
||||
const cycles = db
|
||||
.query(
|
||||
"SELECT * FROM period_cycles WHERE startDate >= $startDate AND startDate < $endDate"
|
||||
)
|
||||
.all({
|
||||
$startDate: startDate,
|
||||
$endDate: endDate,
|
||||
});
|
||||
return cycles as PeriodCycleType[];
|
||||
}
|
||||
|
||||
export function getPeriodCycleByDateRange(startDate: Date, endDate: Date) {
|
||||
const cycles = db
|
||||
.query(
|
||||
"SELECT * FROM period_cycles WHERE startDate >= $startDate AND startDate < $endDate"
|
||||
)
|
||||
.all({
|
||||
$startDate: startDate.toISOString(),
|
||||
$endDate: endDate.toISOString(),
|
||||
});
|
||||
return cycles as PeriodCycleType[];
|
||||
}
|
||||
|
||||
export function createPeriodCycle(
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
ended?: boolean
|
||||
) {
|
||||
db.query(
|
||||
`INSERT INTO period_cycles (id, startDate, endDate, description, ended) VALUES
|
||||
($id, $startDate, $endDate, $description, $ended)`
|
||||
).run({
|
||||
$id: Math.random().toString(36).substring(2, 15),
|
||||
$startDate: startDate.toISOString(),
|
||||
$endDate: endDate.toISOString(),
|
||||
$description: `Started on ${startDate.toISOString()}`,
|
||||
$ended: ended ? 1 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAverageCycleLength() {
|
||||
const cycles = getPeriodCycles();
|
||||
const totalLength = cycles.reduce((acc, cycle) => {
|
||||
const startDate = new Date(cycle.startDate);
|
||||
const endDate = new Date(cycle.endDate);
|
||||
return acc + (endDate.getTime() - startDate.getTime()) / 86400000;
|
||||
}, 0);
|
||||
return totalLength / cycles.length;
|
||||
}
|
||||
|
||||
export function updateEndDatePeriodCycle(id: string, endDate: Date) {
|
||||
db.query("UPDATE period_cycles SET endDate = $endDate WHERE id = $id").run({
|
||||
$id: id,
|
||||
$endDate: endDate.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
export function updateDiscriptionPeriodCycle(id: string, discription: string) {
|
||||
db.query(
|
||||
"UPDATE period_cycles SET description = $description WHERE id = $id"
|
||||
).run({
|
||||
$id: id,
|
||||
$description: discription,
|
||||
});
|
||||
}
|
||||
|
||||
export function endPeriodCycle(id: string, discription?: string) {
|
||||
db.query("UPDATE period_cycles SET ended = 1 WHERE id = $id").run({
|
||||
$id: id,
|
||||
});
|
||||
updateEndDatePeriodCycle(id, new Date());
|
||||
if (discription) {
|
||||
updateDiscriptionPeriodCycle(id, discription);
|
||||
}
|
||||
}
|
||||
|
||||
export function getOngoingPeriodCycle() {
|
||||
const cycle = db.query("SELECT * FROM period_cycles WHERE ended = 0").get();
|
||||
return cycle as PeriodCycleType;
|
||||
}
|
||||
|
||||
export function getPeriodEntries() {
|
||||
const entries = db.query("SELECT * FROM period_entries").get();
|
||||
return entries as PeriodEntryType[];
|
||||
}
|
||||
|
||||
export function getLatestPeriodEntry() {
|
||||
const entry = db
|
||||
.query("SELECT * FROM period_entries ORDER BY date DESC")
|
||||
.get();
|
||||
return entry as PeriodEntryType;
|
||||
}
|
||||
|
||||
export function getPeriodEntriesByDateRange(startDate: Date, endDate: Date) {
|
||||
const entries = db
|
||||
.query(
|
||||
"SELECT * FROM period_entries WHERE date >= $startDate AND date < $endDate"
|
||||
)
|
||||
.all({
|
||||
$startDate: startDate.toISOString(),
|
||||
$endDate: endDate.toISOString(),
|
||||
});
|
||||
return entries as PeriodEntryType[];
|
||||
}
|
||||
|
||||
export function getPeriodEntryByDate(date: Date) {
|
||||
const entry = db
|
||||
.query("SELECT * FROM period_entries WHERE date = $date")
|
||||
.get({ $date: date.toISOString() });
|
||||
return entry as PeriodEntryType;
|
||||
}
|
||||
|
||||
export function updatePeriodEntryByDate(date: Date, description: string) {
|
||||
db.query(
|
||||
"UPDATE period_entries SET description = $description WHERE date = $date"
|
||||
).run({
|
||||
$date: date.toISOString(),
|
||||
$description: description,
|
||||
});
|
||||
}
|
||||
|
||||
export function createPeriodEntry(date: Date, description: string) {
|
||||
db.query(
|
||||
`INSERT INTO period_entries (id, date, description) VALUES
|
||||
($id, $date, $description)`
|
||||
).run({
|
||||
$id: Math.random().toString(36).substring(2, 15),
|
||||
$date: date.toISOString(),
|
||||
$description: description,
|
||||
});
|
||||
}
|
||||
|
||||
// OpenAI tools to manage the cycles
|
||||
|
||||
// Create cycle tool
|
||||
export const CreatePeriodCycleParams = z.object({
|
||||
startDate: z
|
||||
.string()
|
||||
.describe("Date of the start of the period cycle in ISO string format IST"),
|
||||
endDate: z
|
||||
.string()
|
||||
.describe(
|
||||
"The estimated end date of the period cycle. Ask the user how long their period usually lasts and use that data to calculate this. This has to be in ISO string format IST"
|
||||
),
|
||||
});
|
||||
|
||||
export type CreatePeriodCycleParamsType = z.infer<
|
||||
typeof CreatePeriodCycleParams
|
||||
>;
|
||||
|
||||
export async function startNewPeriodCycle({
|
||||
startDate,
|
||||
endDate,
|
||||
}: CreatePeriodCycleParamsType) {
|
||||
if (!startDate || !endDate) {
|
||||
return { error: "startDate and endDate are required" };
|
||||
}
|
||||
|
||||
// Check if there is an ongoing cycle
|
||||
const ongoing = getOngoingPeriodCycle();
|
||||
if (ongoing) {
|
||||
return {
|
||||
error: "There is already an ongoing cycle",
|
||||
ongoingCycle: ongoing,
|
||||
};
|
||||
}
|
||||
|
||||
createPeriodCycle(new Date(startDate), new Date(endDate));
|
||||
return { message: "Started a new period cycle" };
|
||||
}
|
||||
|
||||
// Create old period cycle tool
|
||||
export const CreateOldPeriodCycleParams = z.object({
|
||||
startDate: z
|
||||
.string()
|
||||
.describe("Date of the start of the period cycle in ISO string format IST"),
|
||||
endDate: z
|
||||
.string()
|
||||
.describe(
|
||||
"When did this cycle end. This has to be in ISO string format IST"
|
||||
),
|
||||
});
|
||||
|
||||
export type CreateOldPeriodCycleParamsType = z.infer<
|
||||
typeof CreateOldPeriodCycleParams
|
||||
>;
|
||||
|
||||
export async function createOldPeriodCycle({
|
||||
startDate,
|
||||
endDate,
|
||||
}: CreateOldPeriodCycleParamsType) {
|
||||
if (!startDate || !endDate) {
|
||||
return { error: "startDate and endDate are required" };
|
||||
}
|
||||
|
||||
createPeriodCycle(new Date(startDate), new Date(endDate), true);
|
||||
return { message: "Started a new period cycle" };
|
||||
}
|
||||
|
||||
// Create entry tool
|
||||
export const CreatePeriodEntryParams = z.object({
|
||||
date: z
|
||||
.string()
|
||||
.describe(
|
||||
"Specify a date & time to add a past entry, no need to specify for a new entry"
|
||||
)
|
||||
.default(new Date().toISOString())
|
||||
.optional(),
|
||||
description: z
|
||||
.string()
|
||||
.describe("Description of the vibe the user felt on the day"),
|
||||
});
|
||||
|
||||
export type CreatePeriodEntryParamsType = z.infer<
|
||||
typeof CreatePeriodEntryParams
|
||||
>;
|
||||
|
||||
export async function addOrUpdatePeriodEntryTool({
|
||||
date,
|
||||
description,
|
||||
}: CreatePeriodEntryParamsType) {
|
||||
date = date || (new Date().toISOString() as string);
|
||||
|
||||
try {
|
||||
const cycles = getPeriodCycleByDateRange(
|
||||
new Date(new Date().setFullYear(new Date().getFullYear() - 1)),
|
||||
new Date()
|
||||
);
|
||||
if (cycles.length === 0) {
|
||||
return {
|
||||
error:
|
||||
"You cannot update or add to a cycle that's more than a year old",
|
||||
};
|
||||
}
|
||||
|
||||
const cycle = cycles.find(
|
||||
(cycle) =>
|
||||
new Date(date as string) >= new Date(cycle.startDate) &&
|
||||
new Date(date as string) <= new Date(cycle.endDate)
|
||||
);
|
||||
|
||||
if (!cycle) {
|
||||
console.log(
|
||||
cycle,
|
||||
"error: The specified date does not seem to be part of any existing cycle. Please check the date and/or start a new cycle from this date and try again."
|
||||
);
|
||||
return {
|
||||
error:
|
||||
"The specified date does not seem to be part of any existing cycle. Please check the date and/or start a new cycle from this date and try again.",
|
||||
};
|
||||
}
|
||||
|
||||
createPeriodEntry(new Date(date), description);
|
||||
return {
|
||||
message: "Added a new entry",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
error: "An error occurred while processing the request",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// End cycle tool
|
||||
|
||||
export const EndPeriodCycleParams = z.object({
|
||||
description: z
|
||||
.string()
|
||||
.describe("How did the user feel during this cycle on average"),
|
||||
});
|
||||
|
||||
export type EndPeriodCycleParamsType = z.infer<typeof EndPeriodCycleParams>;
|
||||
|
||||
export async function endPeriodCycleTool({
|
||||
description,
|
||||
}: EndPeriodCycleParamsType) {
|
||||
const ongoingCycle = getOngoingPeriodCycle();
|
||||
const id = ongoingCycle ? ongoingCycle.id : null;
|
||||
|
||||
if (!id) {
|
||||
return { error: "There is no ongoing cycle" };
|
||||
}
|
||||
|
||||
endPeriodCycle(id, description);
|
||||
return { message: "Ended the period cycle" };
|
||||
}
|
||||
|
||||
// Get current cycle tool
|
||||
export const GetCurrentPeriodCycleParams = z.object({});
|
||||
|
||||
export type GetCurrentPeriodCycleParamsType = z.infer<
|
||||
typeof GetCurrentPeriodCycleParams
|
||||
>;
|
||||
|
||||
export async function getCurrentPeriodCycleTool() {
|
||||
try {
|
||||
const cycle = getOngoingPeriodCycle();
|
||||
|
||||
console.log(cycle);
|
||||
|
||||
// Days since period started
|
||||
const noOfDaysSinceStart = Math.floor(
|
||||
(new Date().getTime() - new Date(cycle.startDate).getTime()) / 86400000
|
||||
);
|
||||
|
||||
const averageCycleLength = getAverageCycleLength();
|
||||
|
||||
let note =
|
||||
averageCycleLength > 4
|
||||
? noOfDaysSinceStart > averageCycleLength
|
||||
? "Cycle is overdue"
|
||||
: ""
|
||||
: undefined;
|
||||
|
||||
if (cycle.ended) {
|
||||
note =
|
||||
"There are no ongoing cycles. This is just the last cycle that ended.";
|
||||
}
|
||||
|
||||
if (!cycle.ended) {
|
||||
const endDate = new Date(cycle.endDate);
|
||||
if (endDate < new Date()) {
|
||||
note = "Cycle is overdue, or you forgot to end the cycle.";
|
||||
}
|
||||
}
|
||||
|
||||
const response = {
|
||||
cycle,
|
||||
todaysDate: new Date().toISOString(),
|
||||
noOfDaysSinceStart: cycle.ended ? undefined : noOfDaysSinceStart,
|
||||
averageCycleLength,
|
||||
note,
|
||||
};
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
error: "No ongoing cycle",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get entries in a date range tool
|
||||
export const GetPeriodEntriesParams = z.object({
|
||||
startDate: z.string().describe("Start date in ISO string format IST"),
|
||||
endDate: z.string().describe("End date in ISO string format IST"),
|
||||
});
|
||||
|
||||
export type GetPeriodEntriesParamsType = z.infer<typeof GetPeriodEntriesParams>;
|
||||
|
||||
export async function getPeriodEntriesTool({
|
||||
startDate,
|
||||
endDate,
|
||||
}: GetPeriodEntriesParamsType) {
|
||||
const entries = getPeriodEntriesByDateRange(
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Get vibe by date range tool
|
||||
export const GetVibeByDateRangeParams = z.object({
|
||||
startDate: z.string().describe("Start date in ISO string format IST"),
|
||||
endDate: z.string().describe("End date in ISO string format IST"),
|
||||
});
|
||||
|
||||
export type GetVibeByDateRangeParamsType = z.infer<
|
||||
typeof GetVibeByDateRangeParams
|
||||
>;
|
||||
|
||||
export async function getVibeByDateRangeTool({
|
||||
startDate,
|
||||
endDate,
|
||||
}: GetVibeByDateRangeParamsType) {
|
||||
const entries = getPeriodEntriesByDateRange(
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
|
||||
ask({
|
||||
prompt: `Give me the general summary from the below entries that are a part of a period cycle:
|
||||
----
|
||||
[${entries.map((entry) => entry.description).join("\n")}]
|
||||
----
|
||||
|
||||
The above are entries from ${startDate} to ${endDate}
|
||||
You need to give a general short summary of how the user felt during this period.
|
||||
`,
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Get cycle by date range tool
|
||||
export const GetPeriodCycleByDateRangeParams = z.object({
|
||||
startDate: z.string().describe("Start date in ISO string format IST"),
|
||||
endDate: z.string().describe("End date in ISO string format IST"),
|
||||
});
|
||||
|
||||
export type GetPeriodCycleByDateRangeParamsType = z.infer<
|
||||
typeof GetPeriodCycleByDateRangeParams
|
||||
>;
|
||||
|
||||
export async function getPeriodCycleByDateRangeTool({
|
||||
startDate,
|
||||
endDate,
|
||||
}: GetPeriodCycleByDateRangeParamsType) {
|
||||
const cycles = getPeriodCycleByDateRange(
|
||||
new Date(startDate),
|
||||
new Date(endDate)
|
||||
);
|
||||
return cycles;
|
||||
}
|
||||
|
||||
// Get latest period entry tool
|
||||
export const GetLatestPeriodEntryParams = z.object({});
|
||||
export type GetLatestPeriodEntryParamsType = z.infer<
|
||||
typeof GetLatestPeriodEntryParams
|
||||
>;
|
||||
|
||||
export async function getLatestPeriodEntryTool() {
|
||||
const entry = getLatestPeriodEntry();
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Updated getPeriodTools function
|
||||
export function getPeriodTools(
|
||||
context_message: Message
|
||||
): RunnableToolFunction<any>[] {
|
||||
const userRoles = context_message.getUserRoles();
|
||||
|
||||
if (!userRoles.includes("periodUser")) {
|
||||
// User does not have access to period tools
|
||||
return [];
|
||||
}
|
||||
|
||||
db.close();
|
||||
db = usePrdDb();
|
||||
|
||||
return [
|
||||
zodFunction({
|
||||
function: startNewPeriodCycle,
|
||||
name: "startNewPeriodCycle",
|
||||
schema: CreatePeriodCycleParams,
|
||||
description: `Start a new period cycle.
|
||||
You can specify the start date and end date.
|
||||
You need to ask how the user is feeling and make a period entry about this.`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: createOldPeriodCycle,
|
||||
name: "createOldPeriodCycle",
|
||||
schema: CreateOldPeriodCycleParams,
|
||||
description: `Create a period cycle that has already ended.
|
||||
If the user wants to add entries of older period cycles, you can create a cycle that has already ended.
|
||||
Ask the user for the start date and end date of the cycle in natural language.
|
||||
`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: addOrUpdatePeriodEntryTool,
|
||||
name: "addOrUpdatePeriodEntry",
|
||||
schema: CreatePeriodEntryParams,
|
||||
description: `Add or update a period entry. If the entry for the date already exists, it will be updated.`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: endPeriodCycleTool,
|
||||
name: "endPeriodCycle",
|
||||
schema: EndPeriodCycleParams,
|
||||
description: `End ongoing period cycle. Make sure to confirm with the user before ending the cycle.
|
||||
Ask the user if their cycle needs to be ended if it's been more than 7 days since the start date of the cycle.`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: getCurrentPeriodCycleTool,
|
||||
name: "getCurrentPeriodCycle",
|
||||
schema: GetCurrentPeriodCycleParams,
|
||||
description: `Get the ongoing period cycle.
|
||||
This returns the ongoing period cycle, the number of days since the cycle started, and the average cycle length.
|
||||
`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: getPeriodEntriesTool,
|
||||
name: "getPeriodEntriesByDateRange",
|
||||
schema: GetPeriodEntriesParams,
|
||||
description: "Get period entries in a date range",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getPeriodCycleByDateRangeTool,
|
||||
name: "getPeriodCycleByDateRange",
|
||||
schema: GetPeriodCycleByDateRangeParams,
|
||||
description: "Get period cycles in a date range",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getLatestPeriodEntryTool,
|
||||
name: "getLatestPeriodEntry",
|
||||
schema: GetLatestPeriodEntryParams,
|
||||
description: "Get the latest period entry",
|
||||
}),
|
||||
zodFunction({
|
||||
function: getVibeByDateRangeTool,
|
||||
name: "getVibeByDateRange",
|
||||
schema: GetVibeByDateRangeParams,
|
||||
description: `Get the general vibe of the user in a date range.
|
||||
This will ask the user to give a general summary of how they felt during this period.
|
||||
`,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export function zodFunction<T extends object>({
|
||||
function: fn,
|
||||
schema,
|
||||
description = "",
|
||||
name,
|
||||
}: {
|
||||
function: (args: T) => Promise<object>;
|
||||
schema: ZodSchema<T>;
|
||||
description?: string;
|
||||
name?: string;
|
||||
}): RunnableToolFunctionWithParse<T> {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
function: fn,
|
||||
name: name ?? fn.name,
|
||||
description: description,
|
||||
parameters: zodToJsonSchema(schema) as JSONSchema,
|
||||
parse(input: string): T {
|
||||
const obj = JSON.parse(input);
|
||||
return schema.parse(obj);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Updated startPeriodJob function
|
||||
var jobStarted = false;
|
||||
export function startPeriodJob() {
|
||||
if (jobStarted) return;
|
||||
const timezone = "Asia/Kolkata";
|
||||
cron.schedule(
|
||||
"0 */4 * * *",
|
||||
async () => {
|
||||
console.log("Checking for period entries in the last 2 hours");
|
||||
|
||||
// Get users with 'periodUser' role but not 'testingPeriodUser'
|
||||
const periodUsers = userConfigs.filter(
|
||||
(user) =>
|
||||
user.roles.includes("periodUser") &&
|
||||
!user.roles.includes("testingPeriodUser")
|
||||
);
|
||||
|
||||
for (const user of periodUsers) {
|
||||
// Assuming you have a function to send messages to users based on their identities
|
||||
for (const identity of user.identities) {
|
||||
// Fetch or create a Message object for each user identity
|
||||
const context_message: Message = await getMessageInterface(identity);
|
||||
|
||||
const cycle = getOngoingPeriodCycle();
|
||||
if (!cycle) continue;
|
||||
|
||||
const entry = getLatestPeriodEntry();
|
||||
const isOldEntry =
|
||||
new Date(entry.date) < new Date(new Date().getTime() - 14400000);
|
||||
|
||||
if (isOldEntry) {
|
||||
const message_for_user = await ask({
|
||||
prompt: `Generate a message to remind the user to make a period entry.
|
||||
|
||||
Ask the user how they are feeling about their period cycle as it's been a while since they updated how they felt.
|
||||
Do not explicitly ask them to make an entry, just ask them how they are feeling about their period.
|
||||
|
||||
Today's date: ${new Date().toISOString()}
|
||||
|
||||
Ongoing cycle: ${JSON.stringify(cycle)}
|
||||
|
||||
Note: if the end date is in the past then ask the user if the cycle is still going on or if it's okay to end the cycle.
|
||||
|
||||
Last entry: ${JSON.stringify(entry)}`,
|
||||
});
|
||||
|
||||
if (message_for_user.choices[0].message.content) {
|
||||
await context_message.send({
|
||||
content: message_for_user.choices[0].message.content,
|
||||
});
|
||||
} else {
|
||||
console.log("No message generated");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
timezone,
|
||||
recoverMissedExecutions: true,
|
||||
runOnInit: true,
|
||||
}
|
||||
);
|
||||
jobStarted = true;
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
// tools/python-interpreter.ts
|
||||
|
||||
import { nanoid } from "nanoid";
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { z } from "zod";
|
||||
import { Message } from "../interfaces/message";
|
||||
import QuickChart from "quickchart-js";
|
||||
import { $ } from "zx";
|
||||
|
||||
// Utility function to run Python code in an existing Docker container
|
||||
async function runPythonCodeInExistingDocker(
|
||||
pythonCode: string,
|
||||
dependencies: string[] = [],
|
||||
uid: string
|
||||
) {
|
||||
const containerName = `python-runner-${uid}`;
|
||||
|
||||
// Check if container exists; if not, create it
|
||||
const op =
|
||||
await $`docker ps -a --filter "name=${containerName}" --format "{{.Names}}"`;
|
||||
if (op.stdout.trim() === containerName) {
|
||||
console.log("Container exists");
|
||||
} else {
|
||||
console.log("Container does not exist, creating it");
|
||||
await $`docker run -d --name ${containerName} -v /tmp:/tmp python:3.10 tail -f /dev/null`;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const tempFileName = `tempScript-${nanoid()}.py`;
|
||||
const tempFilePath = path.join(os.tmpdir(), tempFileName);
|
||||
|
||||
fs.writeFile(tempFilePath, pythonCode, (err) => {
|
||||
if (err) {
|
||||
console.error(`Error writing Python script to file: ${err}`);
|
||||
reject(`Error writing Python script to file: ${err}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dockerCommand = `docker exec ${containerName} python /tmp/${tempFileName}`;
|
||||
|
||||
console.log("Executing Docker command:", dockerCommand);
|
||||
|
||||
exec(dockerCommand, (error, stdout, stderr) => {
|
||||
setTimeout(() => {
|
||||
fs.unlink(tempFilePath, (unlinkErr) => {
|
||||
if (unlinkErr) {
|
||||
console.error(`Error deleting temporary file: ${unlinkErr}`);
|
||||
}
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
if (error) {
|
||||
console.error(`Error executing Python script: ${error}`);
|
||||
reject(`${error}\nTry to fix the above error`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
console.log("Python script executed successfully.", stdout);
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Python code interpreter
|
||||
export const PythonCodeParams = z.object({
|
||||
code: z.string().describe("The Python 3.10 code to run"),
|
||||
});
|
||||
export type PythonCodeParams = z.infer<typeof PythonCodeParams>;
|
||||
|
||||
export async function code_interpreter(
|
||||
{ code }: PythonCodeParams,
|
||||
context_message: Message
|
||||
) {
|
||||
try {
|
||||
const output = await runPythonCodeInExistingDocker(
|
||||
code,
|
||||
[],
|
||||
context_message.id
|
||||
);
|
||||
return { output };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
// Run bash command in the above Docker container
|
||||
export const RunPythonCommandParams = z.object({
|
||||
command: z.string().describe("The command to run"),
|
||||
});
|
||||
export type RunPythonCommandParams = z.infer<typeof RunPythonCommandParams>;
|
||||
|
||||
export async function run_command_in_code_interpreter_env(
|
||||
{ command }: RunPythonCommandParams,
|
||||
context_message: Message
|
||||
): Promise<object> {
|
||||
const containerName = `python-runner-${context_message.id}`;
|
||||
|
||||
// Check if container exists; if not, create it
|
||||
const op =
|
||||
await $`docker ps -a --filter "name=${containerName}" --format "{{.Names}}"`;
|
||||
if (op.stdout.trim() === containerName) {
|
||||
console.log("Container exists");
|
||||
} else {
|
||||
console.log("Container does not exist, creating it");
|
||||
await $`docker run -d --name ${containerName} -v /tmp:/tmp python:3.10 tail -f /dev/null`;
|
||||
}
|
||||
|
||||
const dockerCommand = `docker exec ${containerName} ${command}`;
|
||||
|
||||
console.log("Executing Docker command:", dockerCommand);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
exec(dockerCommand, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Error executing command: ${error}`);
|
||||
reject({
|
||||
error: `Error executing command: ${error}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`stderr: ${stderr}`);
|
||||
}
|
||||
console.log("Command executed successfully.");
|
||||
resolve({ output: stdout });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Generate chart image URL using quickchart.io
|
||||
export const ChartParams = z.object({
|
||||
chart_config: z.object({
|
||||
type: z.string().optional(),
|
||||
data: z.object({
|
||||
labels: z.array(z.string()).optional(),
|
||||
datasets: z.array(
|
||||
z.object({
|
||||
label: z.string().optional(),
|
||||
data: z.array(z.number()).optional(),
|
||||
backgroundColor: z.string().optional(),
|
||||
borderColor: z.string().optional(),
|
||||
borderWidth: z.number().optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
title: z.object({
|
||||
display: z.boolean().optional(),
|
||||
text: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
export type ChartParams = z.infer<typeof ChartParams>;
|
||||
|
||||
export async function chart({ chart_config }: ChartParams) {
|
||||
try {
|
||||
const myChart = new QuickChart();
|
||||
myChart.setConfig(chart_config);
|
||||
const chart_url = await myChart.getShortUrl();
|
||||
return { chart_url };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
||||
// Send file to user
|
||||
export const SendFileParams = z.object({
|
||||
file_url: z
|
||||
.string()
|
||||
.describe(
|
||||
"File URL. This can be a web URL or a direct file path from code interpreter '/tmp/file.png'. Try checking if the file exists before sending it."
|
||||
),
|
||||
file_name: z.string().describe("File name, use .png for images"),
|
||||
});
|
||||
export type SendFileParams = z.infer<typeof SendFileParams>;
|
||||
|
||||
export async function send_file(
|
||||
{ file_url, file_name }: SendFileParams,
|
||||
context_message: Message
|
||||
) {
|
||||
try {
|
||||
await context_message.sendFile(file_url, file_name);
|
||||
return { response: "File sent" };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,585 @@
|
|||
import axios, { AxiosError } from "axios";
|
||||
import { z } from "zod";
|
||||
import { zodFunction } from ".";
|
||||
import { RunnableToolFunction } from "openai/lib/RunnableFunction.mjs";
|
||||
import { ask } from "./ask";
|
||||
import { Message } from "../interfaces/message";
|
||||
import { memory_manager_guide, memory_manager_init } from "./memory-manager";
|
||||
|
||||
const NEXTCLOUD_API_ENDPOINT =
|
||||
"http://192.168.29.85/remote.php/dav/calendars/raj/";
|
||||
const NEXTCLOUD_USERNAME = process.env.NEXTCLOUD_USERNAME;
|
||||
const NEXTCLOUD_PASSWORD = process.env.NEXTCLOUD_PASSWORD;
|
||||
|
||||
if (!NEXTCLOUD_USERNAME || !NEXTCLOUD_PASSWORD) {
|
||||
throw new Error(
|
||||
"Please provide NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD environment variables."
|
||||
);
|
||||
}
|
||||
|
||||
const TASKS_CALENDAR_NAME = "anya";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: NEXTCLOUD_API_ENDPOINT,
|
||||
auth: {
|
||||
username: NEXTCLOUD_USERNAME,
|
||||
password: NEXTCLOUD_PASSWORD,
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/xml", // Ensure correct content type for DAV requests
|
||||
},
|
||||
});
|
||||
|
||||
// Schemas for each function's parameters
|
||||
export const TaskParams = z.object({
|
||||
task_id: z.string().describe("The unique ID of the task."),
|
||||
});
|
||||
export type TaskParams = z.infer<typeof TaskParams>;
|
||||
|
||||
export const CreateTaskParams = z.object({
|
||||
summary: z.string().describe("The summary (title) of the task."),
|
||||
description: z.string().optional().describe("The description of the task."),
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The due date of the task in ISO 8601 format."),
|
||||
priority: z.number().optional().describe("The priority of the task (1-9)."),
|
||||
all_day: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether the task is an all-day event."),
|
||||
recurrence: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The recurrence rule for the task in RRULE format."),
|
||||
});
|
||||
export type CreateTaskParams = z.infer<typeof CreateTaskParams>;
|
||||
|
||||
// Functions
|
||||
export async function createTask({
|
||||
summary,
|
||||
description,
|
||||
due_date,
|
||||
priority,
|
||||
all_day,
|
||||
recurrence,
|
||||
}: CreateTaskParams): Promise<object> {
|
||||
const uid = Date.now(); // Use a timestamp as a UID for simplicity
|
||||
|
||||
const formatDueDate = (
|
||||
date: string | undefined,
|
||||
allDay: boolean | undefined
|
||||
) => {
|
||||
if (!date) return "";
|
||||
const d = new Date(date);
|
||||
if (allDay) {
|
||||
return `${d.getUTCFullYear()}${(d.getUTCMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}${d.getUTCDate().toString().padStart(2, "0")}`;
|
||||
} else {
|
||||
return formatDateTime(date);
|
||||
}
|
||||
};
|
||||
|
||||
const icsContent = [
|
||||
"BEGIN:VCALENDAR",
|
||||
"VERSION:2.0",
|
||||
"PRODID:-//Your Company//Your Product//EN",
|
||||
"BEGIN:VTODO",
|
||||
`UID:${uid}@cloud.raj.how`,
|
||||
`SUMMARY:${summary}`,
|
||||
`DESCRIPTION:${description || ""}`,
|
||||
due_date
|
||||
? `${all_day ? "DUE;VALUE=DATE" : "DUE"}:${formatDueDate(
|
||||
due_date,
|
||||
all_day
|
||||
)}\r\n`
|
||||
: "",
|
||||
priority ? `PRIORITY:${priority}` : "",
|
||||
recurrence ? `RRULE:${recurrence}` : "",
|
||||
`DTSTAMP:${formatDateTime(new Date().toISOString())}`,
|
||||
all_day && due_date
|
||||
? `DTSTART;VALUE=DATE:${formatDueDate(due_date, all_day)}`
|
||||
: "",
|
||||
!all_day && due_date ? `DTSTART:${formatDateTime(due_date)}` : "",
|
||||
"STATUS:NEEDS-ACTION",
|
||||
"END:VTODO",
|
||||
"END:VCALENDAR",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\r\n"); // Ensure no empty lines and correct formatting
|
||||
|
||||
try {
|
||||
const response = await apiClient.put(
|
||||
`${TASKS_CALENDAR_NAME}/${uid}.ics`,
|
||||
icsContent,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "text/calendar",
|
||||
},
|
||||
}
|
||||
);
|
||||
return { response: "Task created successfully" };
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Failed to create task:",
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
);
|
||||
return {
|
||||
error: `Error: ${
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const UpdateTaskParams = z.object({
|
||||
task_id: z.string().describe("The unique ID of the task to update."),
|
||||
summary: z.string().optional().describe("The updated summary of the task."),
|
||||
description: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated description of the task."),
|
||||
due_date: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated due date of the task in ISO 8601 format."),
|
||||
priority: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe("The updated priority of the task (1-9)."),
|
||||
all_day: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Whether the task is an all-day event."),
|
||||
recurrence: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("The updated recurrence rule for the task in RRULE format."),
|
||||
});
|
||||
export type UpdateTaskParams = z.infer<typeof UpdateTaskParams>;
|
||||
|
||||
export async function updateTask({
|
||||
task_id,
|
||||
summary,
|
||||
description,
|
||||
due_date,
|
||||
priority,
|
||||
all_day,
|
||||
recurrence,
|
||||
}: UpdateTaskParams): Promise<object> {
|
||||
const existingTaskUrl = `${NEXTCLOUD_API_ENDPOINT}${TASKS_CALENDAR_NAME}/${task_id}.ics`;
|
||||
|
||||
const retryAttempts = 3;
|
||||
const retryDelay = 1000; // 1 second delay between retries
|
||||
|
||||
const formatDueDate = (
|
||||
date: string | undefined,
|
||||
allDay: boolean | undefined
|
||||
) => {
|
||||
if (!date) return "";
|
||||
const d = new Date(date);
|
||||
if (allDay) {
|
||||
return `${d.getUTCFullYear()}${(d.getUTCMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0")}${d.getUTCDate().toString().padStart(2, "0")}`;
|
||||
} else {
|
||||
return formatDateTime(date);
|
||||
}
|
||||
};
|
||||
|
||||
for (let attempt = 1; attempt <= retryAttempts; attempt++) {
|
||||
try {
|
||||
console.log(
|
||||
`Fetching existing task from: ${existingTaskUrl} (Attempt ${attempt})`
|
||||
);
|
||||
const existingTaskResponse = await apiClient.get(existingTaskUrl, {
|
||||
responseType: "text",
|
||||
});
|
||||
|
||||
let existingTaskData = existingTaskResponse.data;
|
||||
|
||||
// Modify the fields in the existing task data
|
||||
if (summary) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
/SUMMARY:.*\r\n/,
|
||||
`SUMMARY:${summary}\r\n`
|
||||
);
|
||||
}
|
||||
if (description) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
/DESCRIPTION:.*\r\n/,
|
||||
`DESCRIPTION:${description}\r\n`
|
||||
);
|
||||
}
|
||||
if (due_date) {
|
||||
const formattedDueDate = formatDueDate(due_date, all_day);
|
||||
const dueRegex = /DUE(;VALUE=DATE)?:.*\r\n/;
|
||||
|
||||
// Replace existing DUE field if it exists, otherwise add it
|
||||
if (dueRegex.test(existingTaskData)) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
dueRegex,
|
||||
`${all_day ? "DUE;VALUE=DATE" : "DUE"}:${formattedDueDate}\r\n`
|
||||
);
|
||||
} else {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
"STATUS:NEEDS-ACTION",
|
||||
`${
|
||||
all_day ? "DUE;VALUE=DATE" : "DUE"
|
||||
}:${formattedDueDate}\r\nSTATUS:NEEDS-ACTION`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (priority) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
/PRIORITY:.*\r\n/,
|
||||
`PRIORITY:${priority}\r\n`
|
||||
);
|
||||
}
|
||||
if (recurrence) {
|
||||
if (existingTaskData.includes("RRULE")) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
/RRULE:.*\r\n/,
|
||||
`RRULE:${recurrence}\r\n`
|
||||
);
|
||||
} else {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
"STATUS:NEEDS-ACTION",
|
||||
`RRULE:${recurrence}\r\nSTATUS:NEEDS-ACTION`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (all_day !== undefined) {
|
||||
const dtstartRegex = /DTSTART(;VALUE=DATE)?:(\d{8}(T\d{6}Z)?)\r\n/;
|
||||
if (all_day) {
|
||||
// If all_day is true, ensure DTSTART is in DATE format
|
||||
if (dtstartRegex.test(existingTaskData)) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
dtstartRegex,
|
||||
`DTSTART;VALUE=DATE:${formatDueDate(due_date, all_day)}\r\n`
|
||||
);
|
||||
} else {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
"STATUS:NEEDS-ACTION",
|
||||
`DTSTART;VALUE=DATE:${formatDueDate(
|
||||
due_date,
|
||||
all_day
|
||||
)}\r\nSTATUS:NEEDS-ACTION`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If all_day is false, ensure DTSTART is in DATE-TIME format
|
||||
if (dtstartRegex.test(existingTaskData)) {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
dtstartRegex,
|
||||
`DTSTART:${formatDateTime(
|
||||
due_date || new Date().toISOString()
|
||||
)}\r\n`
|
||||
);
|
||||
} else {
|
||||
existingTaskData = existingTaskData.replace(
|
||||
"STATUS:NEEDS-ACTION",
|
||||
`DTSTART:${formatDateTime(
|
||||
due_date || new Date().toISOString()
|
||||
)}\r\nSTATUS:NEEDS-ACTION`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Updating task with new data...");
|
||||
const response = await apiClient.put(existingTaskUrl, existingTaskData, {
|
||||
headers: {
|
||||
"Content-Type": "text/calendar",
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Task updated successfully:", response.status);
|
||||
return { response: "Task updated successfully" };
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError;
|
||||
|
||||
// Check for 404 error indicating the task was not found
|
||||
if (axiosError.response?.status === 404) {
|
||||
console.log(`Task not found on attempt ${attempt}. Retrying...`);
|
||||
if (attempt < retryAttempts) {
|
||||
await new Promise((res) => setTimeout(res, retryDelay)); // Wait before retrying
|
||||
continue; // Retry the operation
|
||||
} else {
|
||||
return {
|
||||
error: `Error: Task not found with ID: ${task_id} after ${retryAttempts} attempts. Please check if the task exists.`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Log other errors
|
||||
console.log(
|
||||
`Failed to update task on attempt ${attempt}:`,
|
||||
axiosError.response?.data || axiosError.message
|
||||
);
|
||||
return {
|
||||
error: `Error: ${axiosError.response?.data || axiosError.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback return in case nothing else is returned (should not be reached)
|
||||
return { error: "An unexpected error occurred." };
|
||||
}
|
||||
|
||||
export async function deleteTask({ task_id }: TaskParams) {
|
||||
try {
|
||||
const deleteUrl = `${NEXTCLOUD_API_ENDPOINT}${TASKS_CALENDAR_NAME}/${task_id}.ics`;
|
||||
console.log(`Attempting to delete task at: ${deleteUrl}`);
|
||||
|
||||
const response = await apiClient.delete(deleteUrl);
|
||||
console.log("Task deleted successfully:", response.status);
|
||||
return { response: "Task deleted successfully" };
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Failed to delete task:",
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
);
|
||||
return {
|
||||
error: `Error: ${
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function listTasks({
|
||||
start_time,
|
||||
end_time,
|
||||
}: {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
}): Promise<object> {
|
||||
try {
|
||||
const startISOTime = convertToISOFormat(start_time);
|
||||
const endISOTime = convertToISOFormat(end_time);
|
||||
|
||||
const allTasks: any[] = [];
|
||||
const calendarUrl = `${NEXTCLOUD_API_ENDPOINT}${TASKS_CALENDAR_NAME}/`;
|
||||
console.log(`Accessing tasks calendar URL: ${calendarUrl}`);
|
||||
|
||||
try {
|
||||
const testResponse = await apiClient.get(calendarUrl);
|
||||
console.log(`Test response for ${calendarUrl}: ${testResponse.status}`);
|
||||
} catch (testError) {
|
||||
console.error(
|
||||
`Error accessing ${calendarUrl}: ${(testError as AxiosError).message}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Making REPORT request to ${calendarUrl} for tasks between ${startISOTime} and ${endISOTime}`
|
||||
);
|
||||
|
||||
let reportResponse;
|
||||
try {
|
||||
reportResponse = await apiClient.request({
|
||||
method: "REPORT",
|
||||
url: calendarUrl,
|
||||
headers: { Depth: "1" },
|
||||
data: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<calendar-query xmlns="urn:ietf:params:xml:ns:caldav">
|
||||
<calendar-data/>
|
||||
<filter>
|
||||
<comp-filter name="VCALENDAR">
|
||||
<comp-filter name="VTODO">
|
||||
<time-range start="${startISOTime}" end="${endISOTime}"/>
|
||||
</comp-filter>
|
||||
</comp-filter>
|
||||
</filter>
|
||||
</calendar-query>`,
|
||||
});
|
||||
console.log(`REPORT request successful: Status ${reportResponse.status}`);
|
||||
} catch (reportError) {
|
||||
console.error(
|
||||
`REPORT request failed for ${calendarUrl}: ${
|
||||
(reportError as AxiosError).response?.data ||
|
||||
(reportError as AxiosError).message
|
||||
}`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(`Parsing iCal response for tasks`);
|
||||
const icsFiles = parseICalResponse(reportResponse.data);
|
||||
|
||||
for (const icsFile of icsFiles) {
|
||||
const fullIcsUrl = `http://192.168.29.85${icsFile}?export`;
|
||||
const taskId = icsFile.split("/").pop()?.replace(".ics", "");
|
||||
console.log(`Fetching task data from ${fullIcsUrl}`);
|
||||
|
||||
try {
|
||||
const taskResponse = await apiClient.get(fullIcsUrl, {
|
||||
responseType: "text",
|
||||
});
|
||||
const taskData = taskResponse.data;
|
||||
|
||||
// Identify all-day events
|
||||
const isAllDay = taskData.includes("DTSTART;VALUE=DATE");
|
||||
|
||||
const parsedTask = {
|
||||
task_id: taskId,
|
||||
summary: taskData.match(/SUMMARY:(.*)\r\n/)?.[1],
|
||||
description: taskData.match(/DESCRIPTION:(.*)\r\n/)?.[1],
|
||||
due_date: isAllDay
|
||||
? taskData.match(/DUE;VALUE=DATE:(\d{8})\r\n/)?.[1]
|
||||
: taskData.match(/DUE:(\d{8}T\d{6}Z)/)?.[1],
|
||||
all_day: isAllDay,
|
||||
recurrence: taskData.match(/RRULE:(.*)\r\n/)?.[1],
|
||||
};
|
||||
|
||||
allTasks.push(parsedTask);
|
||||
console.log(
|
||||
`Task data fetched and parsed successfully from ${fullIcsUrl}`
|
||||
);
|
||||
} catch (taskError) {
|
||||
console.error(
|
||||
`Failed to fetch task data from ${fullIcsUrl}: ${
|
||||
(taskError as AxiosError).response?.data ||
|
||||
(taskError as AxiosError).message
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return allTasks;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"Final catch block error:",
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
);
|
||||
return {
|
||||
error: `Error: ${
|
||||
(error as AxiosError).response?.data || (error as AxiosError).message
|
||||
}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert datetime to ISO format in UTC
|
||||
function formatDateTime(dateTime: string): string {
|
||||
const date = new Date(dateTime);
|
||||
const year = date.getUTCFullYear().toString().padStart(4, "0");
|
||||
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getUTCDate().toString().padStart(2, "0");
|
||||
const hours = date.getUTCHours().toString().padStart(2, "0");
|
||||
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
|
||||
const seconds = date.getUTCSeconds().toString().padStart(2, "0");
|
||||
return `${year}${month}${day}T${hours}${minutes}${seconds}Z`; // UTC format required by iCalendar
|
||||
}
|
||||
|
||||
// Integration into runnable tools
|
||||
export let reminders_tools: RunnableToolFunction<any>[] = [
|
||||
zodFunction({
|
||||
function: createTask,
|
||||
name: "createReminder",
|
||||
schema: CreateTaskParams,
|
||||
description: `Create a new task (reminder).
|
||||
|
||||
Before creating a task, run \`listReminders\` to check if it already exists and can be updated instead.
|
||||
|
||||
If a similar task exists, ask the user if they want to update it instead of creating a new one.`,
|
||||
}),
|
||||
zodFunction({
|
||||
function: updateTask,
|
||||
name: "updateReminder",
|
||||
schema: UpdateTaskParams,
|
||||
description: "Update an existing task (reminder).",
|
||||
}),
|
||||
zodFunction({
|
||||
function: deleteTask,
|
||||
name: "deleteReminder",
|
||||
schema: TaskParams,
|
||||
description: "Delete a task (reminder).",
|
||||
}),
|
||||
zodFunction({
|
||||
function: listTasks,
|
||||
name: "listReminders",
|
||||
schema: z.object({
|
||||
start_time: z.string().describe("Start time in ISO 8601 format."),
|
||||
end_time: z.string().describe("End time in ISO 8601 format."),
|
||||
}),
|
||||
description: "List tasks (reminders) within a specified time range.",
|
||||
}),
|
||||
];
|
||||
|
||||
export function getReminderSystemPrompt() {
|
||||
return `Manage your tasks and reminders using these functions to create, update, delete, and list reminders.
|
||||
|
||||
Keep track of important tasks and deadlines programmatically.
|
||||
|
||||
Use correct ISO 8601 time formats and handle task IDs carefully.
|
||||
|
||||
Fetch task data before updating or deleting to avoid errors or duplication.
|
||||
|
||||
Determine a priority level based on the user's tone and urgency of the task.`;
|
||||
}
|
||||
|
||||
// Helper function to convert datetime to ISO format in UTC
|
||||
function convertToISOFormat(dateTime: string): string {
|
||||
const date = new Date(dateTime);
|
||||
return date.toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
|
||||
}
|
||||
|
||||
// Helper function to parse iCal response
|
||||
function parseICalResponse(response: string): string[] {
|
||||
const hrefRegex = /<d:href>([^<]+)<\/d:href>/g;
|
||||
const matches = [];
|
||||
let match;
|
||||
while ((match = hrefRegex.exec(response)) !== null) {
|
||||
matches.push(match[1]);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export const RemindersManagerParams = z.object({
|
||||
request: z.string().describe("User's request regarding reminders or tasks."),
|
||||
});
|
||||
export type RemindersManagerParams = z.infer<typeof RemindersManagerParams>;
|
||||
|
||||
export async function remindersManager(
|
||||
{ request }: RemindersManagerParams,
|
||||
context_message: Message
|
||||
) {
|
||||
const currentTime = new Date();
|
||||
const endOfMonth = new Date(
|
||||
currentTime.getFullYear(),
|
||||
currentTime.getMonth() + 1,
|
||||
0
|
||||
);
|
||||
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `You are a reminders and tasks manager for the 'anya' calendar system.
|
||||
|
||||
Your job is to understand the user's request (e.g., create, update, delete, list reminders) and handle it using the available tools. Use the correct ISO 8601 time format for reminders and provide feedback about the specific action taken.
|
||||
|
||||
----
|
||||
${memory_manager_guide("reminders_manager")}
|
||||
----
|
||||
|
||||
Current Time: ${currentTime.toISOString()}
|
||||
|
||||
This Month's Reminders (${currentTime.toLocaleString()} to ${endOfMonth.toLocaleString()}):
|
||||
${await listTasks({
|
||||
start_time: currentTime.toISOString(),
|
||||
end_time: endOfMonth.toISOString(),
|
||||
})}
|
||||
`,
|
||||
tools: reminders_tools.concat(
|
||||
memory_manager_init(context_message, "reminders_manager")
|
||||
) as any,
|
||||
message: request,
|
||||
seed: `reminders-${context_message.channelId}`,
|
||||
});
|
||||
|
||||
return { response };
|
||||
}
|
|
@ -0,0 +1,237 @@
|
|||
import { z } from "zod";
|
||||
import { Resend } from "resend";
|
||||
import { ask } from "./ask";
|
||||
|
||||
const resend_key = process.env.RESEND_API_KEY?.trim();
|
||||
|
||||
if (!resend_key) {
|
||||
throw new Error("RESEND_API_KEY is required");
|
||||
}
|
||||
|
||||
const resend = new Resend(resend_key);
|
||||
|
||||
export const ResendParams = z.object({
|
||||
to: z.string().email(),
|
||||
subject: z.string(),
|
||||
html: z.string(),
|
||||
});
|
||||
|
||||
export type ResendParams = z.infer<typeof ResendParams>;
|
||||
|
||||
export async function send_email({ to, subject, html }: ResendParams) {
|
||||
if (to.includes("example.com")) {
|
||||
return {
|
||||
error:
|
||||
"Invalid email, this is just an example email please find the user's real email using search user id tool",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await resend.emails.send({
|
||||
from: "anya@tri.raj.how",
|
||||
to,
|
||||
subject,
|
||||
html:
|
||||
(await formatToHtml({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
})) ?? html,
|
||||
});
|
||||
return {
|
||||
response: "Email sent",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const vercel_invite_template = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
|
||||
<head>
|
||||
<link rel="preload" as="image" href="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-logo.png" />
|
||||
<link rel="preload" as="image" href="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-user.png" />
|
||||
<link rel="preload" as="image" href="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-arrow.png" />
|
||||
<link rel="preload" as="image" href="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-team.png" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" /><!--$-->
|
||||
</head>
|
||||
<div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Join Alan on Vercel<div> </div>
|
||||
</div>
|
||||
|
||||
<body style="background-color:rgb(255,255,255);margin-top:auto;margin-bottom:auto;margin-left:auto;margin-right:auto;font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";padding-left:0.5rem;padding-right:0.5rem">
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);border-radius:0.25rem;margin-top:40px;margin-bottom:40px;margin-left:auto;margin-right:auto;padding:20px;max-width:465px">
|
||||
<tbody>
|
||||
<tr style="width:100%">
|
||||
<td>
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="margin-top:32px">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><img alt="Vercel" height="37" src="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-logo.png" style="margin-top:0px;margin-bottom:0px;margin-left:auto;margin-right:auto;display:block;outline:none;border:none;text-decoration:none" width="40" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1 style="color:rgb(0,0,0);font-size:24px;font-weight:400;text-align:center;padding:0px;margin-top:30px;margin-bottom:30px;margin-left:0px;margin-right:0px">Join <strong>Enigma</strong> on <strong>Vercel</strong></h1>
|
||||
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">Hello <!-- -->alanturing<!-- -->,</p>
|
||||
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0"><strong>Alan</strong> (<a href="mailto:alan.turing@example.com" style="color:rgb(37,99,235);text-decoration-line:none;text-decoration:none" target="_blank">alan.turing@example.com</a>) has invited you to the <strong>Enigma</strong> team on<!-- --> <strong>Vercel</strong>.</p>
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation">
|
||||
<tbody style="width:100%">
|
||||
<tr style="width:100%">
|
||||
<td align="right" data-id="__react-email-column"><img height="64" src="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-user.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64" /></td>
|
||||
<td align="center" data-id="__react-email-column"><img alt="invited you to" height="9" src="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-arrow.png" style="display:block;outline:none;border:none;text-decoration:none" width="12" /></td>
|
||||
<td align="left" data-id="__react-email-column"><img height="64" src="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/vercel-team.png" style="border-radius:9999px;display:block;outline:none;border:none;text-decoration:none" width="64" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="text-align:center;margin-top:32px;margin-bottom:32px">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="https://vercel.com/teams/invite/foo" style="background-color:rgb(0,0,0);border-radius:0.25rem;color:rgb(255,255,255);font-size:12px;font-weight:600;text-decoration-line:none;text-align:center;padding-left:1.25rem;padding-right:1.25rem;padding-top:0.75rem;padding-bottom:0.75rem;line-height:100%;text-decoration:none;display:inline-block;max-width:100%;mso-padding-alt:0px;padding:12px 20px 12px 20px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:18" hidden>  </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:9px">Join the team</span><span><!--[if mso]><i style="mso-font-width:500%" hidden>  ​</i><![endif]--></span></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="color:rgb(0,0,0);font-size:14px;line-height:24px;margin:16px 0">or copy and paste this URL into your browser:<!-- --> <a href="https://vercel.com/teams/invite/foo" style="color:rgb(37,99,235);text-decoration-line:none;text-decoration:none" target="_blank">https://vercel.com/teams/invite/foo</a></p>
|
||||
<hr style="border-width:1px;border-style:solid;border-color:rgb(234,234,234);margin-top:26px;margin-bottom:26px;margin-left:0px;margin-right:0px;width:100%;border:none;border-top:1px solid #eaeaea" />
|
||||
<p style="color:rgb(102,102,102);font-size:12px;line-height:24px;margin:16px 0">This invitation was intended for<!-- --> <span style="color:rgb(0,0,0)">alanturing</span>. This invite was sent from <span style="color:rgb(0,0,0)">204.13.186.218</span> <!-- -->located in<!-- --> <span style="color:rgb(0,0,0)">São Paulo, Brazil</span>. If you were not expecting this invitation, you can ignore this email. If you are concerned about your account's safety, please reply to this email to get in touch with us.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table><!--/$-->
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
|
||||
const stripe_welcome_template = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
|
||||
<head>
|
||||
<link rel="preload" as="image" href="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/stripe-logo.png" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" /><!--$-->
|
||||
</head>
|
||||
<div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">You're now ready to make live transactions with Stripe!<div> </div>
|
||||
</div>
|
||||
|
||||
<body style="background-color:#f6f9fc;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif">
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:37.5em;background-color:#ffffff;margin:0 auto;padding:20px 0 48px;margin-bottom:64px">
|
||||
<tbody>
|
||||
<tr style="width:100%">
|
||||
<td>
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:0 48px">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><img alt="Stripe" height="21" src="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/stripe-logo.png" style="display:block;outline:none;border:none;text-decoration:none" width="49" />
|
||||
<hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#e6ebf1;margin:20px 0" />
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">Thanks for submitting your account information. You're now ready to make live transactions with Stripe!</p>
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">You can view your payments and a variety of other information about your account right from your dashboard.</p><a href="https://dashboard.stripe.com/login" style="line-height:100%;text-decoration:none;display:block;max-width:100%;mso-padding-alt:0px;background-color:#656ee8;border-radius:5px;color:#fff;font-size:16px;font-weight:bold;text-align:center;width:100%;padding:10px 10px 10px 10px" target="_blank"><span><!--[if mso]><i style="mso-font-width:500%;mso-text-raise:15" hidden> </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:7.5px">View your Stripe Dashboard</span><span><!--[if mso]><i style="mso-font-width:500%" hidden> ​</i><![endif]--></span></a>
|
||||
<hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#e6ebf1;margin:20px 0" />
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">If you haven't finished your integration, you might find our<!-- --> <a href="https://stripe.com/docs" style="color:#556cd6;text-decoration:none" target="_blank">docs</a> <!-- -->handy.</p>
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">Once you're ready to start accepting payments, you'll just need to use your live<!-- --> <a href="https://dashboard.stripe.com/login?redirect=%2Fapikeys" style="color:#556cd6;text-decoration:none" target="_blank">API keys</a> <!-- -->instead of your test API keys. Your account can simultaneously be used for both test and live requests, so you can continue testing while accepting live payments. Check out our<!-- --> <a href="https://stripe.com/docs/dashboard" style="color:#556cd6;text-decoration:none" target="_blank">tutorial about account basics</a>.</p>
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">Finally, we've put together a<!-- --> <a href="https://stripe.com/docs/checklist/website" style="color:#556cd6;text-decoration:none" target="_blank">quick checklist</a> <!-- -->to ensure your website conforms to card network standards.</p>
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">We'll be here to help you with any step along the way. You can find answers to most questions and get in touch with us on our<!-- --> <a href="https://support.stripe.com/" style="color:#556cd6;text-decoration:none" target="_blank">support site</a>.</p>
|
||||
<p style="font-size:16px;line-height:24px;margin:16px 0;color:#525f7f;text-align:left">— The Stripe team</p>
|
||||
<hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#e6ebf1;margin:20px 0" />
|
||||
<p style="font-size:12px;line-height:16px;margin:16px 0;color:#8898aa">Stripe, 354 Oyster Point Blvd, South San Francisco, CA 94080</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table><!--/$-->
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
|
||||
const linear_login_code_template = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html dir="ltr" lang="en">
|
||||
|
||||
<head>
|
||||
<link rel="preload" as="image" href="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/linear-logo.png" />
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta name="x-apple-disable-message-reformatting" /><!--$-->
|
||||
</head>
|
||||
<div style="display:none;overflow:hidden;line-height:1px;opacity:0;max-height:0;max-width:0">Your login code for Linear<div> </div>
|
||||
</div>
|
||||
|
||||
<body style="background-color:#ffffff;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif">
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="max-width:560px;margin:0 auto;padding:20px 0 48px">
|
||||
<tbody>
|
||||
<tr style="width:100%">
|
||||
<td><img alt="Linear" height="42" src="https://react-email-demo-hbzssj3q3-resend.vercel.app/static/linear-logo.png" style="display:block;outline:none;border:none;text-decoration:none;border-radius:21px;width:42px;height:42px" width="42" />
|
||||
<h1 style="font-size:24px;letter-spacing:-0.5px;line-height:1.3;font-weight:400;color:#484848;padding:17px 0 0">Your login code for Linear</h1>
|
||||
<table align="center" width="100%" border="0" cellPadding="0" cellSpacing="0" role="presentation" style="padding:27px 0 27px">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="https://linear.app" style="line-height:100%;text-decoration:none;display:block;max-width:100%;mso-padding-alt:0px;background-color:#5e6ad2;border-radius:3px;font-weight:600;color:#fff;font-size:15px;text-align:center;padding:11px 23px 11px 23px" target="_blank"><span><!--[if mso]><i style="mso-font-width:383.33333333333337%;mso-text-raise:16.5" hidden>   </i><![endif]--></span><span style="max-width:100%;display:inline-block;line-height:120%;mso-padding-alt:0px;mso-text-raise:8.25px">Login to Linear</span><span><!--[if mso]><i style="mso-font-width:383.33333333333337%" hidden>   ​</i><![endif]--></span></a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size:15px;line-height:1.4;margin:0 0 15px;color:#3c4149">This link and code will only be valid for the next 5 minutes. If the link does not work, you can use the login verification code directly:</p><code style="font-family:monospace;font-weight:700;padding:1px 4px;background-color:#dfe1e4;letter-spacing:-0.3px;font-size:21px;border-radius:4px;color:#3c4149">tt226-5398x</code>
|
||||
<hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#dfe1e4;margin:42px 0 26px" /><a href="https://linear.app" style="color:#b4becc;text-decoration:none;font-size:14px" target="_blank">Linear</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table><!--/$-->
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
|
||||
// use ask function to take some data and pick a relavent template and put the data in it and return the final html string
|
||||
async function formatToHtml({
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
}: {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
}) {
|
||||
const response = await ask({
|
||||
model: "gpt-4o-mini",
|
||||
prompt: `Given some subject and html content
|
||||
|
||||
To: ${to}
|
||||
Subject: ${subject}
|
||||
HTML: ${html}
|
||||
|
||||
Example Templates to Pick from:
|
||||
1. Vercel Invite Template:
|
||||
${vercel_invite_template}
|
||||
|
||||
2. Stripe Welcome Template:
|
||||
${stripe_welcome_template}
|
||||
|
||||
3. Linear Login Code Template:
|
||||
${linear_login_code_template}
|
||||
|
||||
Pick a template and put the data in it to create the final HTML string.
|
||||
Do not Make up false data, use only the given data.
|
||||
|
||||
RETURN ONLY HTML STRING
|
||||
`,
|
||||
});
|
||||
return response.choices[0].message.content
|
||||
? extractHtmlString(response.choices[0].message.content)
|
||||
: null;
|
||||
}
|
||||
|
||||
function extractHtmlString(response: string): string {
|
||||
// Use a regular expression to match the HTML string within the response
|
||||
const htmlMatch = response.match(/<html[^>]*>([\s\S]*?)<\/html>/i);
|
||||
|
||||
// Return the matched HTML content or an empty string if no match is found
|
||||
return htmlMatch ? htmlMatch[0] : "";
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { Message } from "discord.js";
|
||||
import { z } from "zod";
|
||||
|
||||
// get time function
|
||||
const GetTimeParams = z.object({});
|
||||
type GetTimeParams = z.infer<typeof GetTimeParams>;
|
||||
async function get_time({}: GetTimeParams) {
|
||||
return { time: new Date().toLocaleTimeString() };
|
||||
}
|
||||
|
||||
// schedule a message to be sent in the future
|
||||
export const ScheduleMessageParams = z.object({
|
||||
message: z.string(),
|
||||
delay: z.number().describe("delay in milliseconds"),
|
||||
});
|
||||
export type ScheduleMessageParams = z.infer<typeof ScheduleMessageParams>;
|
||||
export async function schedule_message(
|
||||
{ message, delay }: ScheduleMessageParams,
|
||||
context_message: Message<boolean>
|
||||
) {
|
||||
setTimeout(() => {
|
||||
context_message.channel.send(message);
|
||||
}, delay);
|
||||
return { response: "scheduled message" };
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import puppeteer from "puppeteer";
|
||||
import TurndownService from "turndown";
|
||||
import { z } from "zod";
|
||||
import { ask } from "./ask";
|
||||
|
||||
interface ScrapedData {
|
||||
meta: {
|
||||
title: string;
|
||||
description: string;
|
||||
coverImage: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
body: string;
|
||||
}
|
||||
|
||||
async function scrapeAndConvertToMarkdown(url: string): Promise<ScrapedData> {
|
||||
try {
|
||||
// Launch Puppeteer browser
|
||||
const browser = await puppeteer.launch();
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Navigate to the URL
|
||||
await page.goto(url, { waitUntil: "networkidle2" });
|
||||
|
||||
// Extract metadata and body content using Puppeteer
|
||||
const result = await page.evaluate(() => {
|
||||
const meta: any = {
|
||||
title: document.querySelector("head > title")?.textContent || "",
|
||||
description:
|
||||
document
|
||||
.querySelector('meta[name="description"]')
|
||||
?.getAttribute("content") || "",
|
||||
coverImage:
|
||||
document
|
||||
.querySelector('meta[property="og:image"]')
|
||||
?.getAttribute("content") || "",
|
||||
};
|
||||
|
||||
const body = document.querySelector("body")?.innerHTML || "";
|
||||
|
||||
return { meta, body };
|
||||
});
|
||||
|
||||
// Close Puppeteer browser
|
||||
await browser.close();
|
||||
|
||||
// Initialize Turndown service
|
||||
const turndownService = new TurndownService();
|
||||
|
||||
// Convert HTML to Markdown
|
||||
const markdown = turndownService.turndown(result.body);
|
||||
|
||||
// Structure the return object
|
||||
const scrapedData: ScrapedData = {
|
||||
meta: result.meta,
|
||||
body: markdown,
|
||||
};
|
||||
|
||||
return scrapedData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching or converting content:", error);
|
||||
throw new Error("Failed to scrape and convert content");
|
||||
}
|
||||
}
|
||||
|
||||
// Define the parameters schema using Zod
|
||||
export const ScrapeAndConvertToMarkdownParams = z.object({
|
||||
url: z.string().describe("The URL of the webpage to scrape"),
|
||||
summary: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.optional()
|
||||
.describe("Whether to return a summary or the entire content"),
|
||||
summary_instructions: z.string().optional()
|
||||
.describe(`Instructions for summarizing the content.
|
||||
Example: "Return only the author list and their affiliations not the full website content."
|
||||
This can be used to:
|
||||
1. Filter out information that is not needed.
|
||||
2. Scope the summary to a specific part of the content.
|
||||
3. Provide context for the summary.
|
||||
4. Extracting specific information from the content / formatting.
|
||||
`),
|
||||
});
|
||||
|
||||
export type ScrapeAndConvertToMarkdownParams = z.infer<
|
||||
typeof ScrapeAndConvertToMarkdownParams
|
||||
>;
|
||||
|
||||
export async function scrape_and_convert_to_markdown({
|
||||
url,
|
||||
summary,
|
||||
}: ScrapeAndConvertToMarkdownParams) {
|
||||
try {
|
||||
const result = await scrapeAndConvertToMarkdown(url);
|
||||
if (summary) {
|
||||
const summaryResult = await ask({
|
||||
prompt: `Summarize the following content: \n\n${result.body}`,
|
||||
model: "groq-small",
|
||||
});
|
||||
return {
|
||||
meta: result.meta,
|
||||
summary: summaryResult.choices[0].message.content,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { z } from "zod";
|
||||
import { userConfigs } from "../config";
|
||||
import { ask } from "./ask";
|
||||
import { Message } from "../interfaces/message";
|
||||
|
||||
export const SearchUserParams = z.object({
|
||||
name: z.string().describe("The name of the user to search for."),
|
||||
platform: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"The platform to search for the user, this will default to discord."
|
||||
),
|
||||
});
|
||||
|
||||
export type SearchUserParams = z.infer<typeof SearchUserParams>;
|
||||
|
||||
export async function search_user(
|
||||
{ name, platform }: SearchUserParams,
|
||||
context_message: Message
|
||||
) {
|
||||
try {
|
||||
const res = await ask({
|
||||
prompt: `You are a Search Tool that takes in a name and platform and returns the user's details. You are searching for ${name} on ${platform}.
|
||||
|
||||
You need to search for the user in the user config 1st.
|
||||
${JSON.stringify(userConfigs)}
|
||||
|
||||
Return found user in a simple format.
|
||||
`,
|
||||
});
|
||||
console.log(res.choices[0].message);
|
||||
return {
|
||||
response: res.choices[0].message,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import OpenAI from "openai";
|
||||
import { z } from "zod";
|
||||
|
||||
// get meta data from url
|
||||
export const ServiceCheckerParams = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
|
||||
export type ServiceCheckerParams = z.infer<typeof ServiceCheckerParams>;
|
||||
|
||||
const ai_token = process.env.OPENAI_API_KEY?.trim();
|
||||
const openai = new OpenAI({
|
||||
apiKey: ai_token,
|
||||
});
|
||||
|
||||
export async function service_checker({ query }: ServiceCheckerParams) {
|
||||
const status_pages = [
|
||||
"http://192.168.29.85:3001/status/tokio",
|
||||
"https://ark-status.raj.how/status/ark",
|
||||
];
|
||||
|
||||
// fetch the html of the status pages
|
||||
const status_page_html_promises = await Promise.allSettled(
|
||||
status_pages.map(async (url) => {
|
||||
const res = await fetch(url);
|
||||
return res.text();
|
||||
})
|
||||
);
|
||||
|
||||
const status_page_html = status_page_html_promises.map((res) => {
|
||||
if (res.status === "fulfilled") {
|
||||
return res.value;
|
||||
}
|
||||
return res.reason;
|
||||
});
|
||||
|
||||
const res = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are a service status bot that takes in a user query about a service and responds with the status of the service.
|
||||
|
||||
The HTML of the status pages is: "${status_page_html}".
|
||||
|
||||
The user query is: "${query}".
|
||||
`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: query,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
response: res.choices[0].message.content,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
// tool to search whatsapp contacts
|
||||
|
||||
import { z } from "zod";
|
||||
import { whatsappAdapter } from "../interfaces";
|
||||
|
||||
export const SearchContactsParams = z.object({
|
||||
query: z.string().min(3).max(50),
|
||||
});
|
||||
|
||||
export type SearchContactsParams = z.infer<typeof SearchContactsParams>;
|
||||
|
||||
// Function to search contacts
|
||||
export async function search_whatsapp_contacts({
|
||||
query,
|
||||
}: SearchContactsParams) {
|
||||
try {
|
||||
const res = await whatsappAdapter.searchUser(query);
|
||||
return {
|
||||
results: res,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
import OpenAI from "openai";
|
||||
import { TranscriptResponse, YoutubeTranscript } from "youtube-transcript";
|
||||
import { z } from "zod";
|
||||
import ytdl from "ytdl-core";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import { encoding_for_model } from "@dqbd/tiktoken";
|
||||
import { ask } from "./ask";
|
||||
import { pathInDataDir } from "../config";
|
||||
|
||||
export const YoutubeTranscriptParams = z.object({
|
||||
url: z.string(),
|
||||
mode: z
|
||||
.enum(["summary", "full"])
|
||||
.default("summary")
|
||||
.describe(
|
||||
"summary or full transcript if user needs something specific out of the video, use full only when the user explicitly asks for it."
|
||||
),
|
||||
query: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"anything specific to look for in the video, use this for general search queries like 'when was x said in this video' or 'what was the best part of this video'"
|
||||
),
|
||||
});
|
||||
export type YoutubeTranscriptParams = z.infer<typeof YoutubeTranscriptParams>;
|
||||
|
||||
export async function get_youtube_video_data({
|
||||
url,
|
||||
query,
|
||||
mode,
|
||||
}: YoutubeTranscriptParams) {
|
||||
let youtube_meta_data:
|
||||
| {
|
||||
title: string;
|
||||
description: string | null;
|
||||
duration: string;
|
||||
author: string;
|
||||
views: string;
|
||||
publishDate: string;
|
||||
thumbnailUrl: string;
|
||||
}
|
||||
| string;
|
||||
let transcripts: TranscriptResponse[] | string = "";
|
||||
|
||||
try {
|
||||
youtube_meta_data = await getYouTubeVideoMetadata(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
youtube_meta_data = JSON.stringify(error);
|
||||
}
|
||||
|
||||
try {
|
||||
transcripts = await YoutubeTranscript.fetchTranscript(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
transcripts = JSON.stringify(error);
|
||||
}
|
||||
|
||||
const summary =
|
||||
mode === "summary"
|
||||
? await summerize_video(
|
||||
`youtube_meta_data:
|
||||
${
|
||||
typeof youtube_meta_data === "string"
|
||||
? "error fetching meta data, but transcripts maybe available."
|
||||
: JSON.stringify({
|
||||
title: youtube_meta_data.title,
|
||||
description: youtube_meta_data.description,
|
||||
duration: youtube_meta_data.duration,
|
||||
author: youtube_meta_data.author,
|
||||
publishing_date: youtube_meta_data.publishDate,
|
||||
})
|
||||
}
|
||||
|
||||
transcripts:
|
||||
${
|
||||
typeof transcripts === "string"
|
||||
? "error fetching transcripts"
|
||||
: JSON.stringify(transcripts)
|
||||
}
|
||||
`,
|
||||
query
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
youtube_meta_data,
|
||||
summary,
|
||||
transcripts: mode === "full" ? transcripts : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const ai_token = process.env.OPENAI_API_KEY?.trim();
|
||||
|
||||
// save summaries by updating a summary.json file with a hash for the input text
|
||||
function save_summary(text: string, summary: string) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(text);
|
||||
const hash_text = hash.digest("hex");
|
||||
|
||||
const summariesPath = pathInDataDir("summary.json");
|
||||
let summaries: Record<string, string> = {};
|
||||
try {
|
||||
summaries = require(summariesPath);
|
||||
} catch (error) {
|
||||
console.error("Error loading summaries", error);
|
||||
}
|
||||
summaries[hash_text] = summary;
|
||||
fs.writeFileSync(summariesPath, JSON.stringify(summaries));
|
||||
}
|
||||
|
||||
function get_summary(text: string) {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(text);
|
||||
const hash_text = hash.digest("hex");
|
||||
|
||||
const summariesPath = pathInDataDir("summary.json");
|
||||
let summaries = null;
|
||||
try {
|
||||
summaries = fs.readFileSync(summariesPath);
|
||||
} catch (error) {
|
||||
fs.writeFileSync(summariesPath, JSON.stringify({}));
|
||||
}
|
||||
|
||||
if (!summaries) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(summaries.toString())[hash_text];
|
||||
} catch (error) {
|
||||
console.error("Failed to parse summaries", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function numTokensFromString(message: string) {
|
||||
const encoder = encoding_for_model("gpt-3.5-turbo");
|
||||
|
||||
const tokens = encoder.encode(message);
|
||||
encoder.free();
|
||||
return tokens.length;
|
||||
}
|
||||
|
||||
async function summerize_video(text: string, query?: string) {
|
||||
const openai = new OpenAI({
|
||||
apiKey: ai_token,
|
||||
});
|
||||
|
||||
const saved_summary = get_summary(text);
|
||||
|
||||
if (saved_summary && query) {
|
||||
text = saved_summary;
|
||||
} else if (saved_summary) {
|
||||
return saved_summary;
|
||||
}
|
||||
|
||||
const res = await ask({
|
||||
model: "groq-small",
|
||||
prompt: `Summarize all of the youtube info about the given youtube video.
|
||||
|
||||
Youtube Data:
|
||||
-----
|
||||
${text}
|
||||
-----
|
||||
Use the time stamps if available to point out the most important parts of the video or to highlight what the user was looking for in the video.
|
||||
Make sure to link these timed sections so the user can click on the link and directly go to that part of the video.
|
||||
Highlights should include things like something about the topic of the title or the description and not something about the author's self-promotion or the channel itself.
|
||||
`,
|
||||
});
|
||||
|
||||
res.choices[0].message.content &&
|
||||
save_summary(text, res.choices[0].message.content);
|
||||
|
||||
return res.choices[0].message.content;
|
||||
}
|
||||
|
||||
async function getYouTubeVideoMetadata(url: string) {
|
||||
try {
|
||||
const info = await ytdl.getInfo(url);
|
||||
const videoDetails = info.videoDetails;
|
||||
|
||||
const metadata = {
|
||||
title: videoDetails.title,
|
||||
description: videoDetails.description,
|
||||
duration: videoDetails.lengthSeconds, // in seconds
|
||||
author: videoDetails.author.name,
|
||||
views: videoDetails.viewCount,
|
||||
publishDate: videoDetails.publishDate,
|
||||
thumbnailUrl:
|
||||
videoDetails.thumbnails[videoDetails.thumbnails.length - 1]?.url,
|
||||
};
|
||||
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
console.error("Error fetching video metadata:", error);
|
||||
return "Error fetching video metadata";
|
||||
}
|
||||
}
|
||||
|
||||
// youtube downloader tool
|
||||
export const YoutubeDownloaderParams = z.object({
|
||||
url: z.string(),
|
||||
|
||||
quality: z.enum([
|
||||
"highest",
|
||||
"lowest",
|
||||
"highestaudio",
|
||||
"lowestaudio",
|
||||
"highestvideo",
|
||||
"lowestvideo",
|
||||
]),
|
||||
});
|
||||
|
||||
export type YoutubeDownloaderParams = z.infer<typeof YoutubeDownloaderParams>;
|
||||
|
||||
export async function get_download_link({
|
||||
url,
|
||||
quality,
|
||||
}: YoutubeDownloaderParams) {
|
||||
try {
|
||||
const info = await ytdl.getInfo(url);
|
||||
const link = ytdl.chooseFormat(info.formats, { quality: quality });
|
||||
|
||||
return link.url;
|
||||
} catch (error) {
|
||||
console.error("Error fetching video metadata:", error);
|
||||
return "Error fetching video metadata";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { pathInDataDir } from "./config";
|
||||
|
||||
// Define interfaces
|
||||
interface ApiUsage {
|
||||
date: string;
|
||||
model: string;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
}
|
||||
|
||||
interface UsageMetrics {
|
||||
totalPromptTokens: number;
|
||||
totalCompletionTokens: number;
|
||||
totalTokens: number;
|
||||
model: string;
|
||||
}
|
||||
|
||||
// Define the directory for storage
|
||||
const STORAGE_DIR = pathInDataDir("apiUsageData");
|
||||
|
||||
// Ensure the storage directory exists
|
||||
if (!fs.existsSync(STORAGE_DIR)) {
|
||||
fs.mkdirSync(STORAGE_DIR);
|
||||
}
|
||||
|
||||
// Function to get the file path for a specific date
|
||||
function getFilePath(date: string): string {
|
||||
return path.join(STORAGE_DIR, `${date}.json`);
|
||||
}
|
||||
|
||||
// Function to read data from a file
|
||||
function readDataFromFile(filePath: string): ApiUsage[] {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return [];
|
||||
}
|
||||
const rawData = fs.readFileSync(filePath, "utf-8");
|
||||
return JSON.parse(rawData) as ApiUsage[];
|
||||
}
|
||||
|
||||
// Function to write data to a file
|
||||
function writeDataToFile(filePath: string, data: ApiUsage[]): void {
|
||||
const jsonData = JSON.stringify(data, null, 2);
|
||||
fs.writeFileSync(filePath, jsonData, "utf-8");
|
||||
}
|
||||
|
||||
// Function to save API usage data
|
||||
function saveApiUsage(
|
||||
date: string,
|
||||
model: string,
|
||||
promptTokens: number,
|
||||
completionTokens: number
|
||||
): void {
|
||||
const filePath = getFilePath(date);
|
||||
let apiUsageData = readDataFromFile(filePath);
|
||||
let existingData = apiUsageData.find((usage) => usage.model === model);
|
||||
|
||||
if (existingData) {
|
||||
existingData.promptTokens += promptTokens;
|
||||
existingData.completionTokens += completionTokens;
|
||||
existingData.totalTokens += promptTokens + completionTokens;
|
||||
} else {
|
||||
apiUsageData.push({
|
||||
date,
|
||||
model,
|
||||
promptTokens,
|
||||
completionTokens,
|
||||
totalTokens: promptTokens + completionTokens,
|
||||
});
|
||||
}
|
||||
|
||||
writeDataToFile(filePath, apiUsageData);
|
||||
}
|
||||
|
||||
// Function to calculate usage metrics based on a date range
|
||||
function getTotalUsage(fromDate: string, toDate: string): UsageMetrics[] {
|
||||
const from = new Date(fromDate);
|
||||
const to = new Date(toDate);
|
||||
let usageMetrics: { [model: string]: UsageMetrics } = {};
|
||||
|
||||
for (let d = from; d <= to; d.setDate(d.getDate() + 1)) {
|
||||
const filePath = getFilePath(d.toISOString().split("T")[0]);
|
||||
const dailyUsage = readDataFromFile(filePath);
|
||||
|
||||
dailyUsage.forEach((usage) => {
|
||||
if (!usageMetrics[usage.model]) {
|
||||
usageMetrics[usage.model] = {
|
||||
model: usage.model,
|
||||
totalPromptTokens: 0,
|
||||
totalCompletionTokens: 0,
|
||||
totalTokens: 0,
|
||||
};
|
||||
}
|
||||
|
||||
usageMetrics[usage.model].totalPromptTokens += usage.promptTokens;
|
||||
usageMetrics[usage.model].totalCompletionTokens += usage.completionTokens;
|
||||
usageMetrics[usage.model].totalTokens += usage.totalTokens;
|
||||
});
|
||||
}
|
||||
|
||||
return Object.values(usageMetrics);
|
||||
}
|
||||
|
||||
// Function to get total completion tokens for a specific model
|
||||
function getTotalCompletionTokensForModel(
|
||||
model: string,
|
||||
fromDate: string,
|
||||
toDate: string
|
||||
): { promptTokens: number; completionTokens: number } {
|
||||
const from = new Date(fromDate);
|
||||
const to = new Date(toDate);
|
||||
let promptTokens = 0;
|
||||
let completionTokens = 0;
|
||||
|
||||
for (let d = from; d <= to; d.setDate(d.getDate() + 1)) {
|
||||
const filePath = getFilePath(d.toISOString().split("T")[0]);
|
||||
const dailyUsage = readDataFromFile(filePath);
|
||||
|
||||
dailyUsage.forEach((usage) => {
|
||||
if (usage.model === model) {
|
||||
promptTokens += usage.promptTokens;
|
||||
completionTokens += usage.completionTokens;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { promptTokens, completionTokens };
|
||||
}
|
||||
|
||||
// Function to delete usage data older than a specified date
|
||||
function deleteOldUsageData(beforeDate: string): void {
|
||||
const before = new Date(beforeDate);
|
||||
|
||||
fs.readdirSync(STORAGE_DIR).forEach((file) => {
|
||||
const filePath = path.join(STORAGE_DIR, file);
|
||||
const fileDate = file.split(".json")[0];
|
||||
if (new Date(fileDate) < before) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Export the module
|
||||
export {
|
||||
saveApiUsage,
|
||||
getTotalUsage,
|
||||
getTotalCompletionTokensForModel,
|
||||
deleteOldUsageData,
|
||||
ApiUsage,
|
||||
UsageMetrics,
|
||||
};
|
Loading…
Reference in New Issue