1
0

eventHandler.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import { animationList } from "@/utils/paths";
  2. import { loadVRMAnimation } from "@/lib/VRMAnimation/loadVRMAnimation";
  3. import { Chat, ChatConfig } from "@/features/chat/chat";
  4. import { emotions } from "@/features/chat/messages";
  5. import { askLLM } from "@/utils/askLlm";
  6. import { functionCalling } from "@/features/functionCalling/functionCalling";
  7. import { AmicaLife } from "./amicaLife";
  8. import { Viewer } from "../vrmViewer/viewer";
  9. function basename(path: string) {
  10. const a = path.split("/");
  11. return a[a.length - 1];
  12. }
  13. export const idleEvents = [
  14. "VRMA",
  15. "Subconcious",
  16. "IdleTextPrompts",
  17. ] as const;
  18. export const basedPrompt = {
  19. idleTextPrompt: [
  20. "*I am ignoring you*",
  21. "**sighs** It's so quiet here.",
  22. "Tell me something interesting about yourself.",
  23. "**looks around** What do you usually do for fun?",
  24. "I could use a good distraction right now.",
  25. "What's the most fascinating thing you know?",
  26. "If you could talk about anything, what would it be?",
  27. "Got any clever insights to share?",
  28. "**leans in** Any fun stories to tell?",
  29. ],
  30. };
  31. export type AmicaLifeEvents = {
  32. events: string;
  33. };
  34. // Define a constant for max subconcious storage tokens
  35. const MAX_STORAGE_TOKENS = 3000;
  36. // Define the interface for a timestamped prompt
  37. export type TimestampedPrompt = {
  38. prompt: string;
  39. timestamp: string;
  40. }
  41. // Placeholder for storing compressed subconscious prompts
  42. export let storedPrompts: TimestampedPrompt[] = [];
  43. let previousAnimation = "";
  44. // Handles the VRM animation event.
  45. async function handleVRMAnimationEvent(viewer: Viewer, amicaLife: AmicaLife) {
  46. let randomAnimation;
  47. do {
  48. randomAnimation = animationList[Math.floor(Math.random() * animationList.length)];
  49. } while (basename(randomAnimation) === previousAnimation);
  50. // Store the current animation as the previous one for the next call
  51. previousAnimation = basename(randomAnimation);
  52. // removed for staging logs.
  53. //console.log("Handling idle event (animation):", previousAnimation);
  54. try {
  55. if (viewer) {
  56. const animation = await loadVRMAnimation(randomAnimation);
  57. if (!animation) {
  58. throw new Error("Loading animation failed");
  59. }
  60. // @ts-ignore
  61. const duration = await viewer.model!.playAnimation(animation, previousAnimation);
  62. requestAnimationFrame(() => { viewer.resetCameraLerp(); });
  63. // Set timeout for the duration of the animation
  64. setTimeout(() => {
  65. amicaLife.eventProcessing = false;
  66. console.timeEnd("processing_event VRMA");
  67. }, duration * 1000);
  68. }
  69. } catch (error) {
  70. console.error("Error loading animation:", error);
  71. }
  72. }
  73. // Handles text-based idle events.
  74. async function handleTextEvent(chat: Chat, amicaLife: AmicaLife) {
  75. // Randomly select the idle text prompts
  76. const randomIndex = Math.floor(
  77. Math.random() * basedPrompt.idleTextPrompt.length,
  78. );
  79. const randomTextPrompt = basedPrompt.idleTextPrompt[randomIndex];
  80. // removed for staging logs.
  81. //console.log("Handling idle event (text):", randomTextPrompt);
  82. try {
  83. await chat.receiveMessageFromUser?.(randomTextPrompt, true);
  84. amicaLife.eventProcessing = false;
  85. console.timeEnd(`processing_event IdleTextPrompts`);
  86. } catch (error) {
  87. console.error(
  88. "Error occurred while sending a message through chat instance:",
  89. error,
  90. );
  91. }
  92. }
  93. // Handles sleep event.
  94. export async function handleSleepEvent(chat: Chat, amicaLife: AmicaLife) {
  95. console.log("Sleeping...");
  96. amicaLife.pause();
  97. amicaLife.isSleep = true;
  98. try {
  99. const viewer = chat.viewer;
  100. if (viewer) {
  101. // @ts-ignore
  102. await viewer.model!.playEmotion("Sleep");
  103. }
  104. amicaLife.eventProcessing = false;
  105. console.timeEnd("processing_event Sleep");
  106. } catch (error) {
  107. console.error("Error playing emotion sleep:", error);
  108. }
  109. }
  110. // Handles subconcious event.
  111. export async function handleSubconsciousEvent(
  112. config: ChatConfig,
  113. chat: Chat,
  114. amicaLife: AmicaLife,
  115. onChatCompleteResolver?: () => void,
  116. ) {
  117. // removed for staging logs.
  118. // console.log("Handling idle event:", "Subconscious");
  119. const convo = chat.messageList;
  120. const convoLog = convo
  121. .map((message) => {
  122. return `${message.role === "user" ? "User" : "Assistant"}: ${
  123. message.content
  124. }`;
  125. })
  126. .join("\n");
  127. try {
  128. // Step 1: Simulate subconscious self mental diary
  129. const subconciousWordSalad = await askLLM(
  130. config,
  131. "Please reflect on the conversation and let your thoughts flow freely, as if writing a personal diary with events that have occurred:",
  132. `${convoLog}`,
  133. null,
  134. );
  135. // Removed for staging logs.
  136. //console.log("Result from step 1: ", subconciousWordSalad);
  137. // Step 2: Describe the emotion you feel about the subconscious diary
  138. const secondStepPrompt = subconciousWordSalad.startsWith("Error:")
  139. ? convoLog
  140. : subconciousWordSalad;
  141. const decipherEmotion = await askLLM(
  142. config,
  143. "Read this mini-diary, I would like you to simulate a human-like subconscious with deep emotions and describe it from a third-person perspective:",
  144. secondStepPrompt,
  145. null,
  146. );
  147. // Removed for staging logs.
  148. //console.log("Result from step 2: ", decipherEmotion);
  149. // Step 3: Decide on one of the emotion tags best suited for the described emotion
  150. const thirdStepPrompt = decipherEmotion.startsWith("Error:")
  151. ? convoLog
  152. : decipherEmotion;
  153. const emotionDecided = await askLLM(
  154. config,
  155. `Based on your mini-diary, respond with dialougue that sounds like a normal person speaking about their mind, experience or feelings. Make sure to incorporate the specified emotion tags in your response. Here is the list of emotion tags that you have to include in the result : ${emotions
  156. .map((emotion) => `[${emotion}]`)
  157. .join(", ")}:`,
  158. thirdStepPrompt,
  159. chat,
  160. onChatCompleteResolver
  161. );
  162. // Removed for staging logs.
  163. // console.log("Result from step 3: ", emotionDecided);
  164. // Step 4: Compress the subconscious diary entry to 240 characters
  165. const fourthStepPrompt = subconciousWordSalad.startsWith("Error:")
  166. ? convoLog
  167. : subconciousWordSalad;
  168. const compressSubconcious = await askLLM(
  169. config,
  170. "Compress this prompt to 240 characters:",
  171. fourthStepPrompt,
  172. null,
  173. );
  174. console.log("Stored Memory: ", compressSubconcious);
  175. // Add timestamp to the compressed subconscious
  176. const timestampedPrompt: TimestampedPrompt = {
  177. prompt: compressSubconcious,
  178. timestamp: new Date().toISOString(),
  179. };
  180. storedPrompts.push(timestampedPrompt);
  181. let totalStorageTokens = storedPrompts.reduce(
  182. (totalTokens, prompt) => totalTokens + prompt.prompt.length,
  183. 0,
  184. );
  185. while (totalStorageTokens > MAX_STORAGE_TOKENS) {
  186. const removed = storedPrompts.shift();
  187. totalStorageTokens -= removed!.prompt.length;
  188. }
  189. console.log("Stored subconcious prompts:", storedPrompts);
  190. amicaLife.setSubconciousLogs!(storedPrompts);
  191. amicaLife.eventProcessing = false;
  192. console.timeEnd(`processing_event Subconcious`);
  193. } catch (error) {
  194. console.error("Error handling subconscious event:", error);
  195. }
  196. }
  197. // Handles news event
  198. export async function handleNewsEvent(chat: Chat, amicaLife: AmicaLife) {
  199. console.log("Function Calling: News");
  200. try {
  201. const news = await functionCalling("news");
  202. if (!news) {
  203. throw new Error("Loading news failed");
  204. }
  205. await chat.receiveMessageFromUser?.(news, true);
  206. amicaLife.eventProcessing = false;
  207. console.timeEnd("processing_event News");
  208. } catch (error) {
  209. console.error(
  210. "Error occurred while sending a message through chat instance:",
  211. error,
  212. );
  213. }
  214. }
  215. // Main handler for idle events.
  216. export async function handleIdleEvent(
  217. config: ChatConfig,
  218. event: AmicaLifeEvents,
  219. amicaLife: AmicaLife,
  220. chat: Chat,
  221. viewer: Viewer,
  222. ) {
  223. if (!chat) {
  224. console.error("Chat instance is not available");
  225. return;
  226. }
  227. let onChatCompleteResolver: (() => void) | undefined;
  228. const onChatComplete = new Promise<void>((resolve) => {
  229. onChatCompleteResolver = resolve;
  230. });
  231. switch (event.events) {
  232. case "VRMA":
  233. await handleVRMAnimationEvent(viewer, amicaLife);
  234. break;
  235. case "Subconcious":
  236. await handleSubconsciousEvent(config, chat, amicaLife, onChatCompleteResolver!);
  237. await onChatComplete;
  238. break;
  239. case "News":
  240. await handleNewsEvent(chat, amicaLife);
  241. break;
  242. case "Sleep":
  243. await handleSleepEvent(chat, amicaLife);
  244. break;
  245. default:
  246. await handleTextEvent(chat, amicaLife);
  247. break;
  248. }
  249. }