@aj-archipelago/cortex 1.1.31 → 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.
- package/config.js +30 -0
- package/helper-apps/cortex-autogen/.funcignore +8 -0
- package/helper-apps/cortex-autogen/Dockerfile +10 -0
- package/helper-apps/cortex-autogen/OAI_CONFIG_LIST +5 -0
- package/helper-apps/cortex-autogen/function_app.py +29 -0
- package/helper-apps/cortex-autogen/host.json +15 -0
- package/helper-apps/cortex-autogen/main.py +38 -0
- package/helper-apps/cortex-autogen/myautogen.py +228 -0
- package/helper-apps/cortex-autogen/prompt.txt +0 -0
- package/helper-apps/cortex-autogen/prompt_summary.txt +28 -0
- package/helper-apps/cortex-autogen/requirements.txt +8 -0
- package/helper-apps/cortex-autogen/tools/sasfileuploader.py +66 -0
- package/helper-apps/cortex-file-handler/fileChunker.js +9 -2
- package/lib/pathwayTools.js +52 -7
- package/lib/requestExecutor.js +1 -1
- package/lib/util.js +0 -1
- package/package.json +1 -1
- package/pathways/styleguide/styleguide.js +1 -0
- package/pathways/timeline.js +1 -0
- package/server/chunker.js +6 -1
- package/server/modelExecutor.js +4 -0
- package/server/pathwayResolver.js +10 -6
- package/server/plugins/openAiReasoningPlugin.js +61 -0
- package/server/rest.js +74 -47
- package/tests/chunkfunction.test.js +15 -1
package/config.js
CHANGED
|
@@ -154,6 +154,36 @@ var config = convict({
|
|
|
154
154
|
"maxReturnTokens": 4096,
|
|
155
155
|
"supportsStreaming": true
|
|
156
156
|
},
|
|
157
|
+
"oai-o1-mini": {
|
|
158
|
+
"type": "OPENAI-REASONING",
|
|
159
|
+
"url": "https://api.openai.com/v1/chat/completions",
|
|
160
|
+
"headers": {
|
|
161
|
+
"Authorization": "Bearer {{OPENAI_API_KEY}}",
|
|
162
|
+
"Content-Type": "application/json"
|
|
163
|
+
},
|
|
164
|
+
"params": {
|
|
165
|
+
"model": "o1-mini"
|
|
166
|
+
},
|
|
167
|
+
"requestsPerSecond": 10,
|
|
168
|
+
"maxTokenLength": 128000,
|
|
169
|
+
"maxReturnTokens": 65536,
|
|
170
|
+
"supportsStreaming": false
|
|
171
|
+
},
|
|
172
|
+
"oai-o1-preview": {
|
|
173
|
+
"type": "OPENAI-REASONING",
|
|
174
|
+
"url": "https://api.openai.com/v1/chat/completions",
|
|
175
|
+
"headers": {
|
|
176
|
+
"Authorization": "Bearer {{OPENAI_API_KEY}}",
|
|
177
|
+
"Content-Type": "application/json"
|
|
178
|
+
},
|
|
179
|
+
"params": {
|
|
180
|
+
"model": "o1-preview"
|
|
181
|
+
},
|
|
182
|
+
"requestsPerSecond": 10,
|
|
183
|
+
"maxTokenLength": 128000,
|
|
184
|
+
"maxReturnTokens": 32768,
|
|
185
|
+
"supportsStreaming": false
|
|
186
|
+
},
|
|
157
187
|
"azure-bing": {
|
|
158
188
|
"type": "AZURE-BING",
|
|
159
189
|
"url": "https://api.bing.microsoft.com/v7.0/search",
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import azure.functions as func
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
from azure.storage.queue import QueueClient
|
|
5
|
+
import os
|
|
6
|
+
import redis
|
|
7
|
+
from myautogen import process_message
|
|
8
|
+
|
|
9
|
+
app = func.FunctionApp()
|
|
10
|
+
|
|
11
|
+
connection_string = os.environ["AZURE_STORAGE_CONNECTION_STRING"]
|
|
12
|
+
queue_name = os.environ.get("QUEUE_NAME", "autogen-message-queue")
|
|
13
|
+
queue_client = QueueClient.from_connection_string(connection_string, queue_name)
|
|
14
|
+
|
|
15
|
+
redis_client = redis.from_url(os.environ['REDIS_CONNECTION_STRING'])
|
|
16
|
+
channel = 'requestProgress'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.queue_trigger(arg_name="msg", queue_name=queue_name, connection="AZURE_STORAGE_CONNECTION_STRING")
|
|
20
|
+
def queue_trigger(msg: func.QueueMessage):
|
|
21
|
+
logging.info(f"Queue trigger Message ID: {msg.id}")
|
|
22
|
+
try:
|
|
23
|
+
message_data = json.loads(msg.get_body().decode('utf-8'))
|
|
24
|
+
if "requestId" not in message_data:
|
|
25
|
+
message_data['requestId'] = msg.id
|
|
26
|
+
process_message(message_data, msg)
|
|
27
|
+
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logging.error(f"Error processing message: {str(e)}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0",
|
|
3
|
+
"logging": {
|
|
4
|
+
"applicationInsights": {
|
|
5
|
+
"samplingSettings": {
|
|
6
|
+
"isEnabled": true,
|
|
7
|
+
"excludedTypes": "Request"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"extensionBundle": {
|
|
12
|
+
"id": "Microsoft.Azure.Functions.ExtensionBundle",
|
|
13
|
+
"version": "[4.*, 5.0.0)"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from azure.storage.queue import QueueClient
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from myautogen import process_message
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
print("Starting message processing loop")
|
|
10
|
+
connection_string = os.environ["AZURE_STORAGE_CONNECTION_STRING"]
|
|
11
|
+
queue_name = os.environ.get("QUEUE_NAME", "autogen-message-queue")
|
|
12
|
+
|
|
13
|
+
queue_client = QueueClient.from_connection_string(connection_string, queue_name)
|
|
14
|
+
|
|
15
|
+
attempts = 0
|
|
16
|
+
max_attempts = 100
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
while attempts < max_attempts:
|
|
20
|
+
messages = queue_client.receive_messages(messages_per_page=1)
|
|
21
|
+
|
|
22
|
+
if messages:
|
|
23
|
+
for message in messages:
|
|
24
|
+
decoded_content = base64.b64decode(message.content).decode('utf-8')
|
|
25
|
+
message_data = json.loads(decoded_content)
|
|
26
|
+
if "requestId" not in message_data:
|
|
27
|
+
message_data['requestId'] = message.id
|
|
28
|
+
process_message(message_data, message)
|
|
29
|
+
queue_client.delete_message(message)
|
|
30
|
+
attempts = 0 # Reset attempts if a message was processed
|
|
31
|
+
else:
|
|
32
|
+
attempts += 1
|
|
33
|
+
time.sleep(1) # Wait for 1 second before checking again
|
|
34
|
+
|
|
35
|
+
print("No messages received after 100 attempts. Exiting.")
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import azure.functions as func
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
import autogen
|
|
5
|
+
from autogen import AssistantAgent, UserProxyAgent, config_list_from_json, register_function
|
|
6
|
+
from azure.storage.queue import QueueClient
|
|
7
|
+
import os
|
|
8
|
+
import tempfile
|
|
9
|
+
import redis
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
import requests
|
|
12
|
+
import pathlib
|
|
13
|
+
import pymongo
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from tools.sasfileuploader import autogen_sas_uploader
|
|
17
|
+
import shutil
|
|
18
|
+
load_dotenv()
|
|
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
|
+
|
|
39
|
+
app = func.FunctionApp()
|
|
40
|
+
|
|
41
|
+
connection_string = os.environ["AZURE_STORAGE_CONNECTION_STRING"]
|
|
42
|
+
queue_name = os.environ.get("QUEUE_NAME", "autogen-message-queue")
|
|
43
|
+
queue_client = QueueClient.from_connection_string(connection_string, queue_name)
|
|
44
|
+
|
|
45
|
+
redis_client = redis.from_url(os.environ['REDIS_CONNECTION_STRING'])
|
|
46
|
+
channel = 'requestProgress'
|
|
47
|
+
|
|
48
|
+
def connect_redis():
|
|
49
|
+
if not redis_client.ping():
|
|
50
|
+
try:
|
|
51
|
+
redis_client.ping()
|
|
52
|
+
except redis.ConnectionError as e:
|
|
53
|
+
logging.error(f"Error reconnecting to Redis: {e}")
|
|
54
|
+
return False
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
def publish_request_progress(data):
|
|
58
|
+
if connect_redis():
|
|
59
|
+
try:
|
|
60
|
+
message = json.dumps(data)
|
|
61
|
+
logging.info(f"Publishing message {message} to channel {channel}")
|
|
62
|
+
redis_client.publish(channel, message)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logging.error(f"Error publishing message: {e}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_given_system_message():
|
|
68
|
+
env_context = os.environ.get("ENV_SYSTEM_MESSAGE_CONTEXT")
|
|
69
|
+
|
|
70
|
+
if not env_context:
|
|
71
|
+
return read_local_file("prompt.txt")
|
|
72
|
+
|
|
73
|
+
if env_context.startswith(("http://", "https://")):
|
|
74
|
+
return fetch_from_url(env_context)
|
|
75
|
+
|
|
76
|
+
if pathlib.Path(env_context).suffix:
|
|
77
|
+
return read_local_file(env_context)
|
|
78
|
+
|
|
79
|
+
return env_context
|
|
80
|
+
|
|
81
|
+
def read_local_file(filename):
|
|
82
|
+
try:
|
|
83
|
+
with open(filename, "r") as file:
|
|
84
|
+
return file.read()
|
|
85
|
+
except FileNotFoundError:
|
|
86
|
+
logging.error(f"{filename} not found")
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
def fetch_from_url(url):
|
|
90
|
+
try:
|
|
91
|
+
response = requests.get(url)
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
return response.text
|
|
94
|
+
except requests.RequestException as e:
|
|
95
|
+
logging.error(f"Error fetching from URL: {e}")
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
def process_message(message_data, original_request_message):
|
|
99
|
+
logging.info(f"Processing Message: {message_data}")
|
|
100
|
+
try:
|
|
101
|
+
started_at = datetime.now()
|
|
102
|
+
message = message_data['message']
|
|
103
|
+
request_id = message_data.get('requestId') or msg.id
|
|
104
|
+
|
|
105
|
+
config_list = config_list_from_json(env_or_file="OAI_CONFIG_LIST")
|
|
106
|
+
base_url = os.environ.get("CORTEX_API_BASE_URL")
|
|
107
|
+
api_key = os.environ.get("CORTEX_API_KEY")
|
|
108
|
+
llm_config = {"config_list": config_list, "base_url": base_url, "api_key": api_key, "cache_seed": None, "timeout": 600}
|
|
109
|
+
|
|
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
|
+
|
|
114
|
+
code_executor = autogen.coding.LocalCommandLineCodeExecutor(work_dir=temp_dir)
|
|
115
|
+
|
|
116
|
+
message_count = 0
|
|
117
|
+
total_messages = 20 * 2
|
|
118
|
+
all_messages = []
|
|
119
|
+
|
|
120
|
+
def is_termination_msg(m):
|
|
121
|
+
content = m.get("content", "")
|
|
122
|
+
if message_count == 0:
|
|
123
|
+
return False
|
|
124
|
+
return (m.get("role") == "assistant" and not content.strip()) or \
|
|
125
|
+
content.rstrip().endswith("TERMINATE") or \
|
|
126
|
+
"first message must use the" in content.lower() or \
|
|
127
|
+
len(content.strip()) == 0
|
|
128
|
+
|
|
129
|
+
system_message_given = get_given_system_message()
|
|
130
|
+
system_message_assistant = AssistantAgent.DEFAULT_SYSTEM_MESSAGE
|
|
131
|
+
|
|
132
|
+
if system_message_given:
|
|
133
|
+
system_message_assistant = system_message_given
|
|
134
|
+
else:
|
|
135
|
+
print("No extra system message given for assistant")
|
|
136
|
+
|
|
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
|
+
|
|
144
|
+
user_proxy = UserProxyAgent(
|
|
145
|
+
"user_proxy",
|
|
146
|
+
llm_config=llm_config,
|
|
147
|
+
system_message=system_message_given,
|
|
148
|
+
code_execution_config={"executor": code_executor},
|
|
149
|
+
human_input_mode="NEVER",
|
|
150
|
+
max_consecutive_auto_reply=20,
|
|
151
|
+
)
|
|
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
|
+
|
|
171
|
+
original_assistant_send = assistant.send
|
|
172
|
+
original_user_proxy_send = user_proxy.send
|
|
173
|
+
|
|
174
|
+
def logged_send(sender, original_send, message, recipient, request_reply=None, silent=True):
|
|
175
|
+
nonlocal message_count, all_messages
|
|
176
|
+
if not message:
|
|
177
|
+
return
|
|
178
|
+
logging.info(f"Message from {sender.name} to {recipient.name}: {message}")
|
|
179
|
+
message_count += 1
|
|
180
|
+
progress = min(message_count / total_messages, 1)
|
|
181
|
+
all_messages.append({"sender": sender.name, "message": message})
|
|
182
|
+
publish_request_progress({
|
|
183
|
+
"requestId": request_id,
|
|
184
|
+
"progress": progress,
|
|
185
|
+
"info": message
|
|
186
|
+
})
|
|
187
|
+
return original_send(message, recipient, request_reply, silent)
|
|
188
|
+
|
|
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)
|
|
191
|
+
|
|
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})
|
|
194
|
+
|
|
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}"
|
|
202
|
+
|
|
203
|
+
msg = chat_result.summary if chat_result.summary else msg
|
|
204
|
+
|
|
205
|
+
finalData = {
|
|
206
|
+
"requestId": request_id,
|
|
207
|
+
"requestMessage": message_data.get("message"),
|
|
208
|
+
"progress": 1,
|
|
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)
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logging.error(f"Error processing message: {str(e)}")
|
|
223
|
+
if request_id:
|
|
224
|
+
publish_request_progress({
|
|
225
|
+
"requestId": request_id,
|
|
226
|
+
"progress": 1,
|
|
227
|
+
"error": str(e)
|
|
228
|
+
})
|
|
File without changes
|
|
@@ -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: 
|
|
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
|
+
|
|
@@ -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
|
+
|
|
@@ -86,9 +86,16 @@ async function splitMediaFile(inputPath, chunkDurationInSeconds = 500) {
|
|
|
86
86
|
// Extract the original file name from the URL
|
|
87
87
|
const urlObj = new URL(inputPath);
|
|
88
88
|
const originalFileName = path.basename(urlObj.pathname);
|
|
89
|
+
const maxLength = 200; // Set the maximum length for the filename
|
|
90
|
+
let truncatedFileName = originalFileName;
|
|
91
|
+
if (originalFileName.length > maxLength) {
|
|
92
|
+
const extension = path.extname(originalFileName); // Preserve the file extension
|
|
93
|
+
const basename = path.basename(originalFileName, extension); // Get the filename without the extension
|
|
94
|
+
truncatedFileName = basename.substring(0, maxLength) + extension; // Truncate the filename and append the extension
|
|
95
|
+
}
|
|
89
96
|
|
|
90
|
-
// Use the original file name when saving the downloaded file
|
|
91
|
-
const downloadPath = path.join(uniqueOutputPath,
|
|
97
|
+
// Use the original-truncated file name when saving the downloaded file
|
|
98
|
+
const downloadPath = path.join(uniqueOutputPath, truncatedFileName);
|
|
92
99
|
await downloadFile(inputPath, downloadPath);
|
|
93
100
|
inputPath = downloadPath;
|
|
94
101
|
}
|
package/lib/pathwayTools.js
CHANGED
|
@@ -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
|
-
|
|
19
|
+
|
|
16
20
|
const parent = {};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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/requestExecutor.js
CHANGED
|
@@ -236,7 +236,7 @@ const makeRequest = async (cortexRequest) => {
|
|
|
236
236
|
promises.push(selectedEndpoint.limiter.schedule({expiration: pathway.timeout * 1000 + 1000, id: `${requestId}_${uuidv4()}`},() => requestWithMonitor(selectedEndpoint, url, data, axiosConfigObj)));
|
|
237
237
|
} else {
|
|
238
238
|
if (streamRequested) {
|
|
239
|
-
logger.info(`>>> [${requestId}] ${model} does not support streaming - sending non-streaming request`);
|
|
239
|
+
logger.info(`>>> [${requestId}] ${model.name || 'This model'} does not support streaming - sending non-streaming request`);
|
|
240
240
|
axiosConfigObj.params.stream = false;
|
|
241
241
|
data.stream = false;
|
|
242
242
|
}
|
package/lib/util.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aj-archipelago/cortex",
|
|
3
|
-
"version": "1.1.
|
|
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": {
|
package/pathways/timeline.js
CHANGED
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
|
};
|
package/server/modelExecutor.js
CHANGED
|
@@ -17,6 +17,7 @@ import OpenAiEmbeddingsPlugin from './plugins/openAiEmbeddingsPlugin.js';
|
|
|
17
17
|
import OpenAIImagePlugin from './plugins/openAiImagePlugin.js';
|
|
18
18
|
import OpenAIDallE3Plugin from './plugins/openAiDallE3Plugin.js';
|
|
19
19
|
import OpenAIVisionPlugin from './plugins/openAiVisionPlugin.js';
|
|
20
|
+
import OpenAIReasoningPlugin from './plugins/openAiReasoningPlugin.js';
|
|
20
21
|
import GeminiChatPlugin from './plugins/geminiChatPlugin.js';
|
|
21
22
|
import GeminiVisionPlugin from './plugins/geminiVisionPlugin.js';
|
|
22
23
|
import Gemini15ChatPlugin from './plugins/gemini15ChatPlugin.js';
|
|
@@ -82,6 +83,9 @@ class ModelExecutor {
|
|
|
82
83
|
case 'OPENAI-VISION':
|
|
83
84
|
plugin = new OpenAIVisionPlugin(pathway, model);
|
|
84
85
|
break;
|
|
86
|
+
case 'OPENAI-REASONING':
|
|
87
|
+
plugin = new OpenAIReasoningPlugin(pathway, model);
|
|
88
|
+
break;
|
|
85
89
|
case 'GEMINI-CHAT':
|
|
86
90
|
plugin = new GeminiChatPlugin(pathway, model);
|
|
87
91
|
break;
|
|
@@ -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
|
-
|
|
142
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import OpenAIChatPlugin from './openAiChatPlugin.js';
|
|
2
|
+
|
|
3
|
+
class OpenAIReasoningPlugin extends OpenAIChatPlugin {
|
|
4
|
+
|
|
5
|
+
tryParseMessages(messages) {
|
|
6
|
+
let newMessages = [];
|
|
7
|
+
|
|
8
|
+
for (const message of messages) {
|
|
9
|
+
if (message.role === 'user' || message.role === 'assistant') {
|
|
10
|
+
newMessages.push({
|
|
11
|
+
role: message.role,
|
|
12
|
+
content: this.parseContent(message.content)
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
// System messages are simply ignored
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
messages.length = 0;
|
|
19
|
+
messages.push(...newMessages);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
parseContent(content) {
|
|
23
|
+
if (typeof content === 'string') {
|
|
24
|
+
return [{ type: 'text', text: content }];
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(content)) {
|
|
27
|
+
return content.map(item => {
|
|
28
|
+
if (typeof item === 'string') {
|
|
29
|
+
return { type: 'text', text: item };
|
|
30
|
+
}
|
|
31
|
+
const { type, text } = item;
|
|
32
|
+
return { type, text: text || '' };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getRequestParameters(text, parameters, prompt) {
|
|
39
|
+
const requestParameters = super.getRequestParameters(text, parameters, prompt);
|
|
40
|
+
|
|
41
|
+
this.tryParseMessages(requestParameters.messages);
|
|
42
|
+
|
|
43
|
+
const modelMaxReturnTokens = this.getModelMaxReturnTokens();
|
|
44
|
+
const maxTokensPrompt = this.promptParameters.max_tokens;
|
|
45
|
+
const maxTokensModel = this.getModelMaxTokenLength() * (1 - this.getPromptTokenRatio());
|
|
46
|
+
|
|
47
|
+
const maxTokens = maxTokensPrompt || maxTokensModel;
|
|
48
|
+
|
|
49
|
+
requestParameters.max_completion_tokens = maxTokens ? Math.min(maxTokens, modelMaxReturnTokens) : modelMaxReturnTokens;
|
|
50
|
+
requestParameters.temperature = 1;
|
|
51
|
+
|
|
52
|
+
if (this.promptParameters.json) {
|
|
53
|
+
//requestParameters.response_format = { type: "json_object", }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return requestParameters;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default OpenAIReasoningPlugin;
|
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
|
-
|
|
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
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
});
|