@aj-archipelago/cortex 1.1.32 → 1.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,8 @@
1
1
  import azure.functions as func
2
2
  import logging
3
3
  import json
4
- import autogen
5
- from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
6
4
  from azure.storage.queue import QueueClient
7
5
  import os
8
- import tempfile
9
6
  import redis
10
7
  from myautogen import process_message
11
8
 
@@ -26,7 +23,7 @@ def queue_trigger(msg: func.QueueMessage):
26
23
  message_data = json.loads(msg.get_body().decode('utf-8'))
27
24
  if "requestId" not in message_data:
28
25
  message_data['requestId'] = msg.id
29
- process_message(message_data)
26
+ process_message(message_data, msg)
30
27
 
31
28
  except Exception as e:
32
29
  logging.error(f"Error processing message: {str(e)}")
@@ -25,7 +25,7 @@ def main():
25
25
  message_data = json.loads(decoded_content)
26
26
  if "requestId" not in message_data:
27
27
  message_data['requestId'] = message.id
28
- process_message(message_data)
28
+ process_message(message_data, message)
29
29
  queue_client.delete_message(message)
30
30
  attempts = 0 # Reset attempts if a message was processed
31
31
  else:
@@ -2,7 +2,7 @@ import azure.functions as func
2
2
  import logging
3
3
  import json
4
4
  import autogen
5
- from autogen import AssistantAgent, UserProxyAgent, config_list_from_json
5
+ from autogen import AssistantAgent, UserProxyAgent, config_list_from_json, register_function
6
6
  from azure.storage.queue import QueueClient
7
7
  import os
8
8
  import tempfile
@@ -10,9 +10,32 @@ import redis
10
10
  from dotenv import load_dotenv
11
11
  import requests
12
12
  import pathlib
13
-
13
+ import pymongo
14
+ import logging
15
+ from datetime import datetime, timezone
16
+ from tools.sasfileuploader import autogen_sas_uploader
17
+ import shutil
14
18
  load_dotenv()
15
19
 
20
+ DEFAULT_SUMMARY_PROMPT = "Summarize the takeaway from the conversation. Do not add any introductory phrases."
21
+ try:
22
+ with open("prompt_summary.txt", "r") as file:
23
+ summary_prompt = file.read() or DEFAULT_SUMMARY_PROMPT
24
+ except FileNotFoundError:
25
+ summary_prompt = DEFAULT_SUMMARY_PROMPT
26
+
27
+
28
+ def store_in_mongo(data):
29
+ try:
30
+ if 'MONGO_URI' in os.environ:
31
+ client = pymongo.MongoClient(os.environ['MONGO_URI'])
32
+ collection = client.get_default_database()[os.environ.get('MONGO_COLLECTION_NAME', 'autogenruns')]
33
+ collection.insert_one(data)
34
+ else:
35
+ logging.warning("MONGO_URI not found in environment variables")
36
+ except Exception as e:
37
+ logging.error(f"An error occurred while storing data in MongoDB: {str(e)}")
38
+
16
39
  app = func.FunctionApp()
17
40
 
18
41
  connection_string = os.environ["AZURE_STORAGE_CONNECTION_STRING"]
@@ -72,18 +95,22 @@ def fetch_from_url(url):
72
95
  logging.error(f"Error fetching from URL: {e}")
73
96
  return ""
74
97
 
75
- def process_message(message_data):
98
+ def process_message(message_data, original_request_message):
76
99
  logging.info(f"Processing Message: {message_data}")
77
100
  try:
101
+ started_at = datetime.now()
78
102
  message = message_data['message']
79
103
  request_id = message_data.get('requestId') or msg.id
80
104
 
81
105
  config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
82
106
  base_url = os.environ.get("CORTEX_API_BASE_URL")
83
107
  api_key = os.environ.get("CORTEX_API_KEY")
84
- llm_config = {"config_list": config_list, "base_url": base_url, "api_key": api_key, "cache_seed": None}
108
+ llm_config = {"config_list": config_list, "base_url": base_url, "api_key": api_key, "cache_seed": None, "timeout": 600}
85
109
 
86
110
  with tempfile.TemporaryDirectory() as temp_dir:
111
+ #copy /tools directory to temp_dir
112
+ shutil.copytree(os.path.join(os.getcwd(), "tools"), temp_dir, dirs_exist_ok=True)
113
+
87
114
  code_executor = autogen.coding.LocalCommandLineCodeExecutor(work_dir=temp_dir)
88
115
 
89
116
  message_count = 0
@@ -103,26 +130,51 @@ def process_message(message_data):
103
130
  system_message_assistant = AssistantAgent.DEFAULT_SYSTEM_MESSAGE
104
131
 
105
132
  if system_message_given:
106
- system_message_assistant = f"{system_message_assistant}\n\n{system_message_given}"
133
+ system_message_assistant = system_message_given
107
134
  else:
108
135
  print("No extra system message given for assistant")
109
136
 
110
- assistant = AssistantAgent("assistant", llm_config=llm_config, system_message=system_message_assistant)
111
-
137
+ assistant = AssistantAgent("assistant",
138
+ llm_config=llm_config,
139
+ system_message=system_message_assistant,
140
+ code_execution_config={"executor": code_executor},
141
+ is_termination_msg=is_termination_msg,
142
+ )
143
+
112
144
  user_proxy = UserProxyAgent(
113
145
  "user_proxy",
146
+ llm_config=llm_config,
114
147
  system_message=system_message_given,
115
148
  code_execution_config={"executor": code_executor},
116
149
  human_input_mode="NEVER",
117
150
  max_consecutive_auto_reply=20,
118
- is_termination_msg=is_termination_msg,
119
151
  )
120
152
 
153
+ # description = "Upload a file to Azure Blob Storage and get URL back with a SAS token. Requires AZURE_STORAGE_CONNECTION_STRING and AZURE_BLOB_CONTAINER environment variables. Input: file_path (str). Output: SAS URL (str) or error message."
154
+
155
+ # register_function(
156
+ # autogen_sas_uploader,
157
+ # caller=assistant,
158
+ # executor=user_proxy,
159
+ # name="autogen_sas_uploader",
160
+ # description=description,
161
+ # )
162
+
163
+ # register_function(
164
+ # autogen_sas_uploader,
165
+ # caller=user_proxy,
166
+ # executor=assistant,
167
+ # name="autogen_sas_uploader",
168
+ # description=description,
169
+ # )
170
+
121
171
  original_assistant_send = assistant.send
122
172
  original_user_proxy_send = user_proxy.send
123
173
 
124
174
  def logged_send(sender, original_send, message, recipient, request_reply=None, silent=True):
125
175
  nonlocal message_count, all_messages
176
+ if not message:
177
+ return
126
178
  logging.info(f"Message from {sender.name} to {recipient.name}: {message}")
127
179
  message_count += 1
128
180
  progress = min(message_count / total_messages, 1)
@@ -134,19 +186,37 @@ def process_message(message_data):
134
186
  })
135
187
  return original_send(message, recipient, request_reply, silent)
136
188
 
137
- assistant.send = lambda message, recipient, request_reply=None, silent=True: logged_send(assistant, original_assistant_send, message, recipient, request_reply, silent)
138
- user_proxy.send = lambda message, recipient, request_reply=None, silent=True: logged_send(user_proxy, original_user_proxy_send, message, recipient, request_reply, silent)
189
+ assistant.send = lambda message, recipient, request_reply=None, silent=False: logged_send(assistant, original_assistant_send, message, recipient, request_reply, silent)
190
+ user_proxy.send = lambda message, recipient, request_reply=None, silent=False: logged_send(user_proxy, original_user_proxy_send, message, recipient, request_reply, silent)
139
191
 
140
- chat_result = user_proxy.initiate_chat(assistant, message=message)
192
+ #summary_method="reflection_with_llm", "last_msg"
193
+ chat_result = user_proxy.initiate_chat(assistant, message=message, summary_method="reflection_with_llm", summary_args={"summary_role": "user", "summary_prompt": summary_prompt})
141
194
 
142
- msg = all_messages[-3]["message"] if len(all_messages) >= 3 else ""
143
- logging.info(f"####Final message: {msg}")
195
+ msg = ""
196
+ try:
197
+ msg = all_messages[-1 if all_messages[-2]["message"] else -3]["message"]
198
+ logging.info(f"####Final message: {msg}")
199
+ except Exception as e:
200
+ logging.error(f"Error getting final message: {e}")
201
+ msg = f"Finished, with errors 🤖 ... {e}"
144
202
 
145
- publish_request_progress({
203
+ msg = chat_result.summary if chat_result.summary else msg
204
+
205
+ finalData = {
146
206
  "requestId": request_id,
207
+ "requestMessage": message_data.get("message"),
147
208
  "progress": 1,
148
- "data": msg
149
- })
209
+ "data": msg,
210
+ "contextId": message_data.get("contextId"),
211
+ "conversation": all_messages,
212
+ "createdAt": datetime.now(timezone.utc).isoformat(),
213
+ "insertionTime": original_request_message.insertion_time.astimezone(timezone.utc).isoformat() if original_request_message else None,
214
+ "startedAt": started_at.astimezone(timezone.utc).isoformat(),
215
+ }
216
+
217
+ # Final message to indicate completion
218
+ publish_request_progress(finalData)
219
+ store_in_mongo(finalData)
150
220
 
151
221
  except Exception as e:
152
222
  logging.error(f"Error processing message: {str(e)}")
@@ -0,0 +1,28 @@
1
+ Provide a detailed summary of the conversation, including key points, decisions, and action items, and so on.
2
+ Do not add any introductory phrases.
3
+ Avoid expressing gratitude or using pleasantries.
4
+ Maintain a professional and direct tone throughout responses.
5
+ Include most recent meaningful messages from the conversation in the summary.
6
+ You must include all your uploaded URLs, and url of your uploaded final code URL.
7
+ Reply must be in markdown format, including images and videos as UI can show markdown directly to user in a nice way, so make sure to include all visuals, you may do as follows:
8
+ For images: ![Alt Text](IMAGE_URL)
9
+ For videos: <video src="VIDEO_URL" controls></video>
10
+ For urls: [Link Text](URL)
11
+ Your reply will be only thing that finally gets to surface so make sure it is complete.
12
+ Do not mention words like "Summary of the conversation", "Response", "Task", "The conversation" or so as it doesn't makes sense.
13
+ Also no need for "Request", user already know its request and task.
14
+ Be as detailed as possible without being annoying.
15
+ Start with the result as that is the most important part, do not mention "Result" as user already know its result.
16
+ No need to say information about generated SAS urls just include them, only include the latest versions of same file.
17
+ No need to say none of this as user already 'll be aware as has got the result:
18
+ - Code executed successfully, producing correct result ...
19
+ - File uploaded to Azure Blob Storage with unique timestamp ...
20
+ - SAS URL generated for file access, valid for ...
21
+ - File accessibility verified ...
22
+ - Code execution details ...
23
+ - Current date and time ...
24
+ - Script executed twice due to debugging environment ...
25
+ - Verification code ...
26
+ - Issues encountered and resolved: ...
27
+
28
+
@@ -2,5 +2,7 @@ azure-storage-queue
2
2
  azure-functions
3
3
  pyautogen
4
4
  redis
5
+ pymongo
5
6
  requests
6
- azure-storage-blob
7
+ azure-storage-blob
8
+ mysql-connector-python
@@ -0,0 +1,66 @@
1
+ import os
2
+ import sys
3
+ from datetime import datetime, timedelta
4
+ from typing import Annotated
5
+ from pydantic import BaseModel, Field
6
+
7
+ def install_azure_storage_blob():
8
+ import subprocess
9
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "azure-storage-blob"])
10
+
11
+ try:
12
+ from azure.storage.blob import BlobServiceClient, BlobClient, generate_blob_sas, BlobSasPermissions
13
+ except ImportError:
14
+ install_azure_storage_blob()
15
+ from azure.storage.blob import BlobServiceClient, BlobClient, generate_blob_sas, BlobSasPermissions
16
+
17
+ class SasUploaderInput(BaseModel):
18
+ file_path: Annotated[str, Field(description="Path to the file to upload")]
19
+ container_name: Annotated[str, Field(description="Azure Blob container name")]
20
+ blob_name: Annotated[str, Field(description="Name for the blob in Azure storage")]
21
+
22
+ def autogen_sas_uploader(file_path: str) -> str:
23
+ """
24
+ Upload a file to Azure Blob Storage and generate a SAS URL.
25
+
26
+ This function uploads the specified file to Azure Blob Storage using the container name
27
+ from the AZURE_BLOB_CONTAINER environment variable. It then generates and returns a
28
+ Shared Access Signature (SAS) URL for the uploaded blob.
29
+
30
+ Args:
31
+ file_path (str): Path to the local file to be uploaded.
32
+
33
+ Returns:
34
+ str: SAS URL of the uploaded blob if successful, or an error message if the upload fails.
35
+
36
+ Note:
37
+ - Requires AZURE_STORAGE_CONNECTION_STRING and AZURE_BLOB_CONTAINER environment variables.
38
+ - The blob name in Azure will be the same as the input file name.
39
+ """
40
+ connect_str = os.environ.get('AZURE_STORAGE_CONNECTION_STRING')
41
+ container_name = os.environ.get('AZURE_BLOB_CONTAINER')
42
+
43
+ if not connect_str or not container_name:
44
+ return "Error: AZURE_STORAGE_CONNECTION_STRING or AZURE_BLOB_CONTAINER not set."
45
+
46
+ blob_service_client = BlobServiceClient.from_connection_string(connect_str)
47
+ blob_client = blob_service_client.get_blob_client(container=container_name, blob=file_path)
48
+
49
+ try:
50
+ with open(file_path, "rb") as data:
51
+ blob_client.upload_blob(data, overwrite=True)
52
+
53
+ sas_token = generate_blob_sas(
54
+ account_name=blob_service_client.account_name,
55
+ container_name=container_name,
56
+ blob_name=file_path,
57
+ account_key=blob_service_client.credential.account_key,
58
+ permission=BlobSasPermissions(read=True),
59
+ expiry=datetime.utcnow() + timedelta(days=30)
60
+ )
61
+
62
+ sas_url = f"https://{blob_service_client.account_name}.blob.core.windows.net/{container_name}/{file_path}?{sas_token}"
63
+ return sas_url
64
+ except Exception as e:
65
+ return f"Error uploading file: {str(e)}"
66
+
@@ -1,6 +1,10 @@
1
1
  // pathwayTools.js
2
2
  import { encode, decode } from '../lib/encodeCache.js';
3
3
  import { config } from '../config.js';
4
+ import { publishRequestProgress } from "../lib/redisSubscription.js";
5
+ import { getSemanticChunks } from "../server/chunker.js";
6
+ import logger from '../lib/logger.js';
7
+ import { requestState } from '../server/requestState.js';
4
8
 
5
9
  // callPathway - call a pathway from another pathway
6
10
  const callPathway = async (pathwayName, inArgs, pathwayResolver) => {
@@ -12,14 +16,26 @@ const callPathway = async (pathwayName, inArgs, pathwayResolver) => {
12
16
  if (!pathway) {
13
17
  throw new Error(`Pathway ${pathwayName} not found`);
14
18
  }
15
- const requestState = {};
19
+
16
20
  const parent = {};
17
- const data = await pathway.rootResolver(parent, args, { config, pathway, requestState } );
18
-
19
- // Merge the results into the pathwayResolver if it was provided
20
- if (pathwayResolver) {
21
- pathwayResolver.mergeResults(data);
21
+ let rootRequestId = pathwayResolver?.rootRequestId || pathwayResolver?.requestId;
22
+
23
+ let data = await pathway.rootResolver(parent, {...args, rootRequestId}, { config, pathway, requestState } );
24
+
25
+ if (args.async || args.stream) {
26
+ const { result: requestId } = data;
27
+
28
+ // Fire the resolver for the async requestProgress
29
+ logger.info(`Callpathway starting async requestProgress, requestId: ${requestId}`);
30
+ const { resolver, args } = requestState[requestId];
31
+ requestState[requestId].useRedis = false;
32
+ requestState[requestId].started = true;
33
+
34
+ data = resolver && await resolver(args);
22
35
  }
36
+
37
+ // Update pathwayResolver with new data if available
38
+ pathwayResolver?.mergeResults(data);
23
39
 
24
40
  return data?.result;
25
41
  };
@@ -32,4 +48,33 @@ const gpt3Decode = (text) => {
32
48
  return decode(text);
33
49
  }
34
50
 
35
- export { callPathway, gpt3Encode, gpt3Decode };
51
+ const say = async (requestId, message, maxMessageLength = Infinity) => {
52
+ try {
53
+ const chunks = getSemanticChunks(message, maxMessageLength);
54
+
55
+ for (let chunk of chunks) {
56
+ await publishRequestProgress({
57
+ requestId,
58
+ progress: 0.5,
59
+ data: chunk
60
+ });
61
+ }
62
+
63
+ await publishRequestProgress({
64
+ requestId,
65
+ progress: 0.5,
66
+ data: " ... "
67
+ });
68
+
69
+ await publishRequestProgress({
70
+ requestId,
71
+ progress: 0.5,
72
+ data: "\n\n"
73
+ });
74
+
75
+ } catch (error) {
76
+ logger.error(`Say error: ${error.message}`);
77
+ }
78
+ };
79
+
80
+ export { callPathway, gpt3Encode, gpt3Decode, say };
package/lib/util.js CHANGED
@@ -2,7 +2,6 @@ import logger from "./logger.js";
2
2
  import stream from 'stream';
3
3
  import subsrt from 'subsrt';
4
4
  import os from 'os';
5
- import path from 'path';
6
5
  import http from 'http';
7
6
  import https from 'https';
8
7
  import { URL } from 'url';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aj-archipelago/cortex",
3
- "version": "1.1.32",
3
+ "version": "1.1.33",
4
4
  "description": "Cortex is a GraphQL API for AI. It provides a simple, extensible interface for using AI services from OpenAI, Azure and others.",
5
5
  "private": false,
6
6
  "repository": {
@@ -1,4 +1,5 @@
1
1
  import { Prompt } from '../../server/prompt.js';
2
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
3
  import * as Diff from "diff";
3
4
 
4
5
  const prompt = new Prompt({
@@ -1,5 +1,6 @@
1
1
  import { Prompt } from '../server/prompt.js';
2
2
  import * as chrono from 'chrono-node';
3
+ // eslint-disable-next-line import/no-extraneous-dependencies
3
4
  import dayjs from 'dayjs';
4
5
 
5
6
  const getLastOccurrenceOfMonth = (month) => {
package/server/chunker.js CHANGED
@@ -217,6 +217,11 @@ const semanticTruncate = (text, maxLength) => {
217
217
  : truncatedText + "...";
218
218
  };
219
219
 
220
+ const getSingleTokenChunks = (text) => {
221
+ if (text === '') return [''];
222
+ return encode(text).map(token => decode([token]));
223
+ }
224
+
220
225
  export {
221
- getSemanticChunks, semanticTruncate, getLastNToken, getFirstNToken, determineTextFormat
226
+ getSemanticChunks, semanticTruncate, getLastNToken, getFirstNToken, determineTextFormat, getSingleTokenChunks
222
227
  };
@@ -27,6 +27,7 @@ class PathwayResolver {
27
27
  this.warnings = [];
28
28
  this.errors = [];
29
29
  this.requestId = uuidv4();
30
+ this.rootRequestId = null;
30
31
  this.responseParser = new PathwayResponseParser(pathway);
31
32
  this.tool = null;
32
33
  this.modelName = [
@@ -84,7 +85,7 @@ class PathwayResolver {
84
85
  catch (error) {
85
86
  if (!args.async) {
86
87
  publishRequestProgress({
87
- requestId: this.requestId,
88
+ requestId: this.rootRequestId || this.requestId,
88
89
  progress: 1,
89
90
  data: '[DONE]',
90
91
  });
@@ -100,9 +101,9 @@ class PathwayResolver {
100
101
  // some models don't support progress updates
101
102
  if (!modelTypesExcludedFromProgressUpdates.includes(this.model.type)) {
102
103
  await publishRequestProgress({
103
- requestId: this.requestId,
104
+ requestId: this.rootRequestId || this.requestId,
104
105
  progress: completedCount / totalCount,
105
- data: JSON.stringify(responseData),
106
+ data: typeof responseData === 'string' ? responseData : JSON.stringify(responseData),
106
107
  });
107
108
  }
108
109
  // If the response is an object, it's a streaming response
@@ -113,7 +114,7 @@ class PathwayResolver {
113
114
 
114
115
  const onParse = (event) => {
115
116
  let requestProgress = {
116
- requestId: this.requestId
117
+ requestId: this.rootRequestId || this.requestId
117
118
  };
118
119
 
119
120
  logger.debug(`Received event: ${event.type}`);
@@ -138,8 +139,10 @@ class PathwayResolver {
138
139
 
139
140
  try {
140
141
  if (!streamEnded && requestProgress.data) {
141
- //logger.info(`Publishing stream message to requestId ${this.requestId}: ${message}`);
142
- publishRequestProgress(requestProgress);
142
+ if (!(this.rootRequestId && requestProgress.progress === 1)) {
143
+ logger.debug(`Publishing stream message to requestId ${this.requestId}: ${requestProgress.data}`);
144
+ publishRequestProgress(requestProgress);
145
+ }
143
146
  streamEnded = requestProgress.progress === 1;
144
147
  }
145
148
  } catch (error) {
@@ -195,6 +198,7 @@ class PathwayResolver {
195
198
  if (!requestState[this.requestId]) {
196
199
  requestState[this.requestId] = {}
197
200
  }
201
+ this.rootRequestId = args.rootRequestId ?? null;
198
202
  requestState[this.requestId] = { ...requestState[this.requestId], args, resolver: this.asyncResolve.bind(this) };
199
203
  return this.requestId;
200
204
  }
package/server/rest.js CHANGED
@@ -5,7 +5,20 @@ import pubsub from './pubsub.js';
5
5
  import { requestState } from './requestState.js';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import logger from '../lib/logger.js';
8
-
8
+ import { getSingleTokenChunks } from './chunker.js';
9
+
10
+ const chunkTextIntoTokens = (() => {
11
+ let partialToken = '';
12
+ return (text, isLast = false, useSingleTokenStream = false) => {
13
+ const tokens = useSingleTokenStream ? getSingleTokenChunks(partialToken + text) : [text];
14
+ if (isLast) {
15
+ partialToken = '';
16
+ return tokens;
17
+ }
18
+ partialToken = useSingleTokenStream ? tokens.pop() : '';
19
+ return tokens;
20
+ };
21
+ })();
9
22
 
10
23
  const processRestRequest = async (server, req, pathway, name, parameterMap = {}) => {
11
24
  const fieldVariableDefs = pathway.typeDef(pathway).restDefinition || [];
@@ -50,7 +63,8 @@ const processRestRequest = async (server, req, pathway, name, parameterMap = {})
50
63
  return resultText;
51
64
  };
52
65
 
53
- const processIncomingStream = (requestId, res, jsonResponse) => {
66
+ const processIncomingStream = (requestId, res, jsonResponse, pathway) => {
67
+ const useSingleTokenStream = pathway.useSingleTokenStream || false;
54
68
 
55
69
  const startStream = (res) => {
56
70
  // Set the headers for streaming
@@ -61,6 +75,14 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
61
75
  }
62
76
 
63
77
  const finishStream = (res, jsonResponse) => {
78
+ // Send the last partial token if it exists
79
+ const lastTokens = chunkTextIntoTokens('', true, useSingleTokenStream);
80
+ if (lastTokens.length > 0) {
81
+ lastTokens.forEach(token => {
82
+ fillJsonResponse(jsonResponse, token, null);
83
+ sendStreamData(jsonResponse);
84
+ });
85
+ }
64
86
 
65
87
  // If we haven't sent the stop message yet, do it now
66
88
  if (jsonResponse.choices?.[0]?.finish_reason !== "stop") {
@@ -85,11 +107,11 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
85
107
  }
86
108
 
87
109
  const sendStreamData = (data) => {
88
- logger.debug(`REST SEND: data: ${JSON.stringify(data)}`);
89
110
  const dataString = (data==='[DONE]') ? data : JSON.stringify(data);
90
111
 
91
112
  if (!res.writableEnded) {
92
113
  res.write(`data: ${dataString}\n\n`);
114
+ logger.debug(`REST SEND: data: ${dataString}`);
93
115
  }
94
116
  }
95
117
 
@@ -115,63 +137,68 @@ const processIncomingStream = (requestId, res, jsonResponse) => {
115
137
  if (subscription) {
116
138
  try {
117
139
  const subPromiseResult = await subscription;
118
- if (subPromiseResult) {
119
- pubsub.unsubscribe(subPromiseResult);
120
- }
140
+ subPromiseResult && pubsub.unsubscribe(subPromiseResult);
121
141
  } catch (error) {
122
142
  logger.error(`Error unsubscribing from pubsub: ${error}`);
123
143
  }
124
144
  }
125
145
  }
126
146
 
127
- if (data.requestProgress.requestId === requestId) {
128
- logger.debug(`REQUEST_PROGRESS received progress: ${data.requestProgress.progress}, data: ${data.requestProgress.data}`);
129
-
130
- const progress = data.requestProgress.progress;
131
- const progressData = data.requestProgress.data;
147
+ if (data.requestProgress.requestId !== requestId) return;
132
148
 
133
- try {
134
- const messageJson = JSON.parse(progressData);
135
- if (messageJson.error) {
136
- logger.error(`Stream error REST: ${messageJson?.error?.message || 'unknown error'}`);
137
- safeUnsubscribe();
138
- finishStream(res, jsonResponse);
139
- return;
140
- } else if (messageJson.choices) {
141
- const { text, delta, finish_reason } = messageJson.choices[0];
149
+ logger.debug(`REQUEST_PROGRESS received progress: ${data.requestProgress.progress}, data: ${data.requestProgress.data}`);
150
+
151
+ const { progress, data: progressData } = data.requestProgress;
142
152
 
143
- if (messageJson.object === 'text_completion') {
144
- fillJsonResponse(jsonResponse, text, finish_reason);
145
- } else {
146
- fillJsonResponse(jsonResponse, delta.content, finish_reason);
147
- }
148
- } else if (messageJson.candidates) {
149
- const { content, finishReason } = messageJson.candidates[0];
150
- fillJsonResponse(jsonResponse, content.parts[0].text, finishReason);
151
- } else if (messageJson.content) {
152
- const text = messageJson.content?.[0]?.text || '';
153
- const finishReason = messageJson.stop_reason;
154
- fillJsonResponse(jsonResponse, text, finishReason);
155
- } else {
156
- fillJsonResponse(jsonResponse, messageJson, null);
157
- }
158
- } catch (error) {
159
- //logger.info(`progressData not JSON: ${progressData}`);
160
- fillJsonResponse(jsonResponse, progressData, "stop");
161
- }
162
- if (progress === 1 && progressData.trim() === "[DONE]") {
153
+ try {
154
+ const messageJson = JSON.parse(progressData);
155
+ if (messageJson.error) {
156
+ logger.error(`Stream error REST: ${messageJson?.error?.message || 'unknown error'}`);
163
157
  safeUnsubscribe();
164
158
  finishStream(res, jsonResponse);
165
159
  return;
166
160
  }
167
161
 
168
- sendStreamData(jsonResponse);
162
+ let content = '';
163
+ if (messageJson.choices) {
164
+ const { text, delta } = messageJson.choices[0];
165
+ content = messageJson.object === 'text_completion' ? text : delta.content;
166
+ } else if (messageJson.candidates) {
167
+ content = messageJson.candidates[0].content.parts[0].text;
168
+ } else if (messageJson.content) {
169
+ content = messageJson.content?.[0]?.text || '';
170
+ } else {
171
+ content = messageJson;
172
+ }
169
173
 
170
- if (progress === 1) {
171
- safeUnsubscribe();
172
- finishStream(res, jsonResponse);
174
+ chunkTextIntoTokens(content, false, useSingleTokenStream).forEach(token => {
175
+ fillJsonResponse(jsonResponse, token, null);
176
+ sendStreamData(jsonResponse);
177
+ });
178
+ } catch (error) {
179
+ logger.debug(`progressData not JSON: ${progressData}`);
180
+ if (typeof progressData === 'string') {
181
+ if (progress === 1 && progressData.trim() === "[DONE]") {
182
+ fillJsonResponse(jsonResponse, progressData, "stop");
183
+ safeUnsubscribe();
184
+ finishStream(res, jsonResponse);
185
+ return;
186
+ }
187
+
188
+ chunkTextIntoTokens(progressData, false, useSingleTokenStream).forEach(token => {
189
+ fillJsonResponse(jsonResponse, token, null);
190
+ sendStreamData(jsonResponse);
191
+ });
192
+ } else {
193
+ fillJsonResponse(jsonResponse, progressData, "stop");
194
+ sendStreamData(jsonResponse);
173
195
  }
174
196
  }
197
+
198
+ if (progress === 1) {
199
+ safeUnsubscribe();
200
+ finishStream(res, jsonResponse);
201
+ }
175
202
  });
176
203
 
177
204
  // Fire the resolver for the async requestProgress
@@ -254,7 +281,7 @@ function buildRestEndpoints(pathways, app, server, config) {
254
281
  jsonResponse.choices[0].finish_reason = null;
255
282
  //jsonResponse.object = "text_completion.chunk";
256
283
 
257
- processIncomingStream(resultText, res, jsonResponse);
284
+ processIncomingStream(resultText, res, jsonResponse, pathway);
258
285
  } else {
259
286
  const requestId = uuidv4();
260
287
  jsonResponse.id = `cmpl-${requestId}`;
@@ -306,7 +333,7 @@ function buildRestEndpoints(pathways, app, server, config) {
306
333
  }
307
334
  jsonResponse.object = "chat.completion.chunk";
308
335
 
309
- processIncomingStream(resultText, res, jsonResponse);
336
+ processIncomingStream(resultText, res, jsonResponse, pathway);
310
337
  } else {
311
338
  const requestId = uuidv4();
312
339
  jsonResponse.id = `chatcmpl-${requestId}`;
@@ -346,4 +373,4 @@ function buildRestEndpoints(pathways, app, server, config) {
346
373
  }
347
374
  }
348
375
 
349
- export { buildRestEndpoints };
376
+ export { buildRestEndpoints };
@@ -1,5 +1,5 @@
1
1
  import test from 'ava';
2
- import { getSemanticChunks, determineTextFormat } from '../server/chunker.js';
2
+ import { getSemanticChunks, determineTextFormat, getSingleTokenChunks } from '../server/chunker.js';
3
3
  import { encode } from '../lib/encodeCache.js';
4
4
 
5
5
  const testText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. In id erat sem. Phasellus ac dapibus purus, in fermentum nunc. Mauris quis rutrum magna. Quisque rutrum, augue vel blandit posuere, augue magna convallis turpis, nec elementum augue mauris sit amet nunc. Aenean sit amet leo est. Nunc ante ex, blandit et felis ut, iaculis lacinia est. Phasellus dictum orci id libero ullamcorper tempor.
@@ -207,4 +207,18 @@ test('should return identical text that chunker was passed, given weird spaces a
207
207
  t.assert(chunks.every(chunk => encode(chunk).length <= maxChunkToken)); //check chunk size
208
208
  const recomposedText = chunks.reduce((acc, chunk) => acc + chunk, '');
209
209
  t.assert(recomposedText === testTextShortWeirdSpaces); //check recomposition
210
+ });
211
+
212
+ test('should correctly split text into single token chunks', t => {
213
+ const testString = 'Hello, world!';
214
+ const chunks = getSingleTokenChunks(testString);
215
+
216
+ // Check that each chunk is a single token
217
+ t.true(chunks.every(chunk => encode(chunk).length === 1));
218
+
219
+ // Check that joining the chunks recreates the original string
220
+ t.is(chunks.join(''), testString);
221
+
222
+ // Check specific tokens (this may need adjustment based on your tokenizer)
223
+ t.deepEqual(chunks, ['Hello', ',', ' world', '!']);
210
224
  });
@@ -1,93 +0,0 @@
1
- import os
2
- import sys
3
- from datetime import datetime, timedelta
4
-
5
- def install_azure_storage_blob():
6
- print("Installing azure-storage-blob...")
7
- import subprocess
8
- subprocess.check_call([sys.executable, "-m", "pip", "install", "azure-storage-blob"])
9
- print("azure-storage-blob installed successfully.")
10
-
11
- try:
12
- from azure.storage.blob import BlobServiceClient, BlobClient, generate_blob_sas, BlobSasPermissions
13
- except ImportError:
14
- install_azure_storage_blob()
15
- from azure.storage.blob import BlobServiceClient, BlobClient, generate_blob_sas, BlobSasPermissions
16
-
17
- def generate_sas_url(blob_service_client, container_name, blob_name):
18
- """
19
- Generates a SAS URL for a blob.
20
- """
21
- sas_token = generate_blob_sas(
22
- account_name=blob_service_client.account_name,
23
- container_name=container_name,
24
- blob_name=blob_name,
25
- account_key=blob_service_client.credential.account_key,
26
- permission=BlobSasPermissions(read=True, write=True),
27
- expiry=datetime.utcnow() + timedelta(hours=1)
28
- )
29
- return f"https://{blob_service_client.account_name}.blob.core.windows.net/{container_name}/{blob_name}?{sas_token}"
30
-
31
- def upload_file_to_blob(file_path, sas_url):
32
- """
33
- Uploads a single file to Azure Blob Storage using a SAS URL.
34
- """
35
- try:
36
- blob_client = BlobClient.from_blob_url(sas_url)
37
- with open(file_path, "rb") as data:
38
- blob_client.upload_blob(data, overwrite=True)
39
- print(f"Successfully uploaded {os.path.basename(file_path)} to Azure Blob Storage.")
40
- return True
41
- except Exception as e:
42
- print(f"Error uploading file: {e}")
43
- return False
44
-
45
- def main():
46
- # Get Azure Storage connection string from environment variable
47
- connect_str = os.environ.get('AZURE_STORAGE_CONNECTION_STRING')
48
- if not connect_str:
49
- print("Error: AZURE_STORAGE_CONNECTION_STRING is not set in environment variables.")
50
- sys.exit(1)
51
-
52
- # Create the BlobServiceClient object
53
- blob_service_client = BlobServiceClient.from_connection_string(connect_str)
54
-
55
- # Get the container name from environment variable or use a default
56
- container_name = os.environ.get('AZURE_BLOB_CONTAINER', 'testcontainer')
57
-
58
- # Test file details
59
- file_path = "/tmp/test_file.txt"
60
- blob_name = "test_file.txt"
61
-
62
- # Create a test file
63
- with open(file_path, "w") as f:
64
- f.write("This is a test file for Azure Blob Storage upload.")
65
-
66
- print(f"Test file created at: {file_path}")
67
-
68
- # Generate SAS URL
69
- sas_url = generate_sas_url(blob_service_client, container_name, blob_name)
70
- print(f"Generated SAS URL: {sas_url}")
71
-
72
- # Upload file
73
- if upload_file_to_blob(file_path, sas_url):
74
- print("File upload completed successfully.")
75
- else:
76
- print("File upload failed.")
77
-
78
- # Clean up the test file
79
- os.remove(file_path)
80
- print(f"Test file removed: {file_path}")
81
-
82
- # Upload this script to Azure Blob Storage
83
- script_path = os.path.abspath(__file__)
84
- script_name = os.path.basename(script_path)
85
- script_sas_url = generate_sas_url(blob_service_client, container_name, script_name)
86
-
87
- if upload_file_to_blob(script_path, script_sas_url):
88
- print(f"Script uploaded successfully. You can access it at: {script_sas_url}")
89
- else:
90
- print("Failed to upload the script.")
91
-
92
- if __name__ == "__main__":
93
- main()