sanjay920 commited on
Commit
29f7f08
1 Parent(s): 94a5758
Files changed (8) hide show
  1. README.md +18 -7
  2. app.py +328 -46
  3. install_node.sh +16 -0
  4. packages.txt +2 -0
  5. postprocess.py +156 -0
  6. preprocess.py +256 -0
  7. requirements.txt +11 -1
  8. style.css +35 -0
README.md CHANGED
@@ -1,13 +1,24 @@
1
  ---
2
- title: Rubra V0.1
3
- emoji: 💬
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 4.36.1
8
- app_file: app.py
9
  pinned: false
10
  license: apache-2.0
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- An example chatbot using [Gradio](https://gradio.app), [`huggingface_hub`](https://huggingface.co/docs/huggingface_hub/v0.22.2/en/index), and the [Hugging Face Inference API](https://huggingface.co/docs/api-inference/index).
 
 
 
 
1
  ---
2
+ title: Rubra v0.1
3
+ emoji: 🦙
4
+ colorFrom: indigo
5
+ colorTo: pink
6
  sdk: gradio
7
+ sdk_version: 4.37.1
 
8
  pinned: false
9
  license: apache-2.0
10
+ thumbnail: https://rubra.ai/
11
+ suggested_hardware: a10g-large
12
+ # preload_from_hub:
13
+ # - rubra-ai/Meta-Llama-3-8B-Instruct
14
+ # - rubra-ai/Phi-3-mini-128k-instruct
15
+ # - rubra-ai/Mistral-7B-Instruct-v0.3
16
+ # - rubra-ai/Mistral-7B-Instruct-v0.2
17
+ # - rubra-ai/gemma-1.1-2b-it
18
+ # - rubra-ai/Qwen2-7B-Instruct
19
  ---
20
 
21
+ # Rubra v0.1 - A Collection of Tool (Function) Calling LLMs
22
+
23
+ This Space demonstrates Rubra tool calling models. Please, check the original model cards for details.
24
+
app.py CHANGED
@@ -1,63 +1,345 @@
 
 
 
 
 
1
  import gradio as gr
2
- from huggingface_hub import InferenceClient
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  """
5
- For more information on `huggingface_hub` Inference API support, please check the docs: https://huggingface.co/docs/huggingface_hub/v0.22.2/en/guides/inference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  """
7
- client = InferenceClient("HuggingFaceH4/zephyr-7b-beta")
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- def respond(
11
- message,
12
- history: list[tuple[str, str]],
13
- system_message,
14
- max_tokens,
15
- temperature,
16
- top_p,
17
- ):
18
- messages = [{"role": "system", "content": system_message}]
 
 
 
 
 
19
 
20
- for val in history:
21
- if val[0]:
22
- messages.append({"role": "user", "content": val[0]})
23
- if val[1]:
24
- messages.append({"role": "assistant", "content": val[1]})
25
 
26
- messages.append({"role": "user", "content": message})
 
 
 
27
 
28
- response = ""
 
 
29
 
30
- for message in client.chat_completion(
31
- messages,
32
- max_tokens=max_tokens,
33
- stream=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  temperature=temperature,
35
- top_p=top_p,
36
- ):
37
- token = message.choices[0].delta.content
 
 
38
 
39
- response += token
40
- yield response
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  """
43
- For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
44
- """
45
- demo = gr.ChatInterface(
46
- respond,
47
- additional_inputs=[
48
- gr.Textbox(value="You are a friendly Chatbot.", label="System message"),
49
- gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max new tokens"),
50
- gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
51
- gr.Slider(
52
- minimum=0.1,
53
- maximum=1.0,
54
- value=0.95,
55
- step=0.05,
56
- label="Top-p (nucleus sampling)",
57
- ),
58
- ],
59
- )
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  if __name__ == "__main__":
63
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from threading import Thread
3
+ from typing import Iterator
4
+ import json
5
+
6
  import gradio as gr
7
+ import spaces
8
+ import torch
9
+ from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
10
+ import subprocess
11
+ import copy
12
+
13
+ import subprocess
14
+ import sys
15
+
16
+ def run_command(command):
17
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
18
+ output, error = process.communicate()
19
+ if process.returncode != 0:
20
+ print(f"Error executing command: {command}")
21
+ print(f"Error message: {error.decode('utf-8')}")
22
+ sys.exit(1)
23
+ return output.decode('utf-8')
24
+
25
+ MAX_MAX_NEW_TOKENS = 2048
26
+ DEFAULT_MAX_NEW_TOKENS = 1024
27
+ MAX_INPUT_TOKEN_LENGTH = int(os.getenv("MAX_INPUT_TOKEN_LENGTH", "8000"))
28
+ model_choices = [
29
+ "rubra-ai/Meta-Llama-3-8B-Instruct",
30
+ "rubra-ai/Qwen2-7B-Instruct",
31
+ "rubra-ai/Phi-3-mini-128k-instruct",
32
+ "rubra-ai/Mistral-7B-Instruct-v0.3",
33
+ "rubra-ai/Mistral-7B-Instruct-v0.2",
34
+ "rubra-ai/gemma-1.1-2b-it"
35
+ ]
36
+
37
+ DESCRIPTION = """\
38
+ # Rubra v0.1 - Top LLMs enhanced with function (tool) calling
39
+
40
+ This is a demo of the Rubra collection of models. You can use the models for general conversation,
41
+ task completion, and function calling with the provided tools input.
42
 
43
  """
44
+
45
+ LICENSE = """
46
+ <p/>
47
+
48
+ ---
49
+ Rubra code is licensed under the Apache License, Version 2.0 (the "License");
50
+ you may not use this file except in compliance with the License.
51
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
52
+
53
+ Unless required by applicable law or agreed to in writing, software
54
+ distributed under the License is distributed on an "AS IS" BASIS,
55
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
56
+ See the License for the specific language governing permissions and
57
+ limitations under the License.
58
+
59
+ Rubra models are licensed under the parent model's license. See the parent model card for more information.
60
  """
 
61
 
62
+ if not torch.cuda.is_available():
63
+ DESCRIPTION += "\n<p>Running on CPU 🥶 This demo does not work on CPU.</p>"
64
+
65
+ if torch.cuda.is_available():
66
+ model_id = "sanjay920/Llama-3-8b-function-calling-alpha-v1" # Default model
67
+ model = None
68
+ tokenizer = None
69
+
70
+ def load_model(model_name):
71
+ global model, tokenizer
72
+ model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto", load_in_4bit=False)
73
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
74
+ tokenizer.use_default_system_prompt = False
75
+ model.generation_config.pad_token_id = tokenizer.pad_token_id
76
+
77
+ load_model(model_id) # Load the default model
78
+
79
+ def is_valid_json(tools: str) -> bool:
80
+ try:
81
+ json.loads(tools)
82
+ return True
83
+ except ValueError:
84
+ return False
85
+
86
+ def validate_tools(tools):
87
+ if tools.strip() == "" or is_valid_json(tools):
88
+ return gr.update(visible=False)
89
+ else:
90
+ return gr.update(visible=True)
91
 
92
+ def json_to_markdown(json_obj):
93
+ """Convert a JSON object to a formatted markdown string."""
94
+ markdown = ""
95
+ for item in json_obj:
96
+ if item.get("type") == "text":
97
+ # For text items, just add the text content
98
+ markdown += item.get("text", "") + "\n\n"
99
+ elif item.get("type") == "function":
100
+ # For function calls, format as JSON
101
+ markdown += "```json\n"
102
+ # markdown += json.dumps(item.get("function", {}), indent=2)
103
+ markdown += json.dumps(item, indent=2)
104
+ markdown += "\n```\n\n"
105
+ return markdown.strip()
106
 
107
+ def user(user_message, history):
108
+ return "", history + [[user_message, None]]
 
 
 
109
 
110
+ def bot(history, system_prompt, tools, role, max_new_tokens, temperature):
111
+ user_message = history[-1][0]
112
+ if history[-1][1] is None:
113
+ history[-1][1] = "" # Ensure it's never None
114
 
115
+ ui_history = list(history) # Clone the history for UI updates
116
+ all_tool_outputs = [] # Store all processed outputs for final aggregation
117
+ output_accumulated = "" # To accumulate outputs before processing
118
 
119
+ for chunk in generate(user_message, history[:-1], system_prompt, tools, role, max_new_tokens, temperature):
120
+ history[-1][1] += chunk
121
+ print(history[-1][1])
122
+
123
+ if "endtoolcall" in history[-1][1]:
124
+ process_output = postprocess_output(history[-1][1])
125
+ print("process output:\n", process_output)
126
+ if process_output:
127
+ temp_history = copy.deepcopy(history) # Use deepcopy here
128
+ if isinstance(process_output, list) and len(process_output) > 0 and isinstance(process_output[0], dict):
129
+ markdown_output = json_to_markdown(process_output)
130
+ temp_history[-1][1] = markdown_output
131
+ else:
132
+ temp_history[-1][1] = str(process_output)
133
+ print(temp_history[-1][1])
134
+ print("--------------------------")
135
+ yield temp_history
136
+ else:
137
+ print(history[-1][1])
138
+ print("--------------------------")
139
+ yield history
140
+ else:
141
+ print(history[-1][1])
142
+ print("--------------------------")
143
+ yield history
144
+
145
+ @spaces.GPU
146
+ def generate(
147
+ message: str,
148
+ chat_history: list[tuple[str, str]],
149
+ system_prompt: str,
150
+ tools: str,
151
+ role: str,
152
+ max_new_tokens: int = 1024,
153
+ temperature: float = 0.6,
154
+ ) -> Iterator[str]:
155
+ global model, tokenizer
156
+ conversation = []
157
+ if system_prompt:
158
+ conversation.append({"role": "system", "content": system_prompt})
159
+ for user, assistant in chat_history:
160
+ conversation.extend([{"role": "user", "content": user}, {"role": "assistant", "content": assistant}])
161
+ conversation.append({"role": role, "content": message})
162
+
163
+ if tools:
164
+ if not is_valid_json(tools):
165
+ yield "Invalid JSON in tools. Please correct it."
166
+ return
167
+ tools = json.loads(tools)
168
+ formatted_msgs = preprocess_input(msgs=conversation, tools=tools)
169
+ else:
170
+ formatted_msgs = conversation
171
+
172
+ input_ids = tokenizer.apply_chat_template(formatted_msgs, return_tensors="pt")
173
+ if input_ids.shape[1] > MAX_INPUT_TOKEN_LENGTH:
174
+ input_ids = input_ids[:, -MAX_INPUT_TOKEN_LENGTH:]
175
+ gr.Warning(f"Trimmed input from conversation as it was longer than {MAX_INPUT_TOKEN_LENGTH} tokens.")
176
+ input_ids = input_ids.to(model.device)
177
+
178
+ streamer = TextIteratorStreamer(tokenizer, timeout=10.0, skip_prompt=True, skip_special_tokens=True)
179
+ generate_kwargs = dict(
180
+ input_ids=input_ids,
181
+ streamer=streamer,
182
+ max_new_tokens=max_new_tokens,
183
+ do_sample=True,
184
+ top_p=0.95,
185
  temperature=temperature,
186
+ num_beams=1,
187
+ repetition_penalty=1.2,
188
+ )
189
+ t = Thread(target=model.generate, kwargs=generate_kwargs)
190
+ t.start()
191
 
192
+ for text in streamer:
193
+ # print("Generated text:", text)
194
+ yield text
195
 
196
+ bot_message = """Hello! How can I assist you today? If you have any questions or need information on a specific topic, feel free to ask. I can also utilize `tools` that you input to help you better. For example:
197
+ ```
198
+ [
199
+ {
200
+ "type": "function",
201
+ "function": {
202
+ "name": "get_current_weather",
203
+ "description": "Get the current weather in a given location",
204
+ "parameters": {
205
+ "type": "object",
206
+ "properties": {
207
+ "location": {
208
+ "type": "string",
209
+ "description": "Must include the city AND state, e.g. 'San Francisco, CA'"
210
+ },
211
+ "unit": {
212
+ "type": "string",
213
+ "enum":
214
+ ["celsius", "fahrenheit"]
215
+ }
216
+ },
217
+ "required": ["location"]
218
+ }
219
+ }
220
+ }
221
+ ]
222
+ ```
223
+
224
+ You can also define `functions` (deprecated in favor of `tools` in OpenAI):
225
+ ```
226
+ [
227
+ {
228
+ "name": "get_current_date",
229
+ "description": "Gets the current date at the given location. Results are in ISO 8601 date format; e.g. 2024-04-25",
230
+ "parameters": {
231
+ "type": "object",
232
+ "properties": {
233
+ "location": {
234
+ "type": "string",
235
+ "description": "The city and state to get the current date at, e.g. San Francisco, CA"
236
+ }
237
+ },
238
+ "required":["location"]
239
+ }
240
+ }
241
+ ]
242
+ ```
243
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
+ def create_chat_interface():
246
+ with gr.Blocks(css="style.css") as demo:
247
+ gr.Markdown(DESCRIPTION)
248
+
249
+ with gr.Row(equal_height=True, elem_id="main-row"):
250
+ with gr.Column(scale=3, min_width=500):
251
+ # Initialize the chatbot with the welcome message
252
+ chatbot = gr.Chatbot(
253
+ value=[("Hi", bot_message)],
254
+ show_copy_button=True,
255
+ elem_id="chatbot",
256
+ show_label=False,
257
+ render_markdown=True,
258
+ height="100%",
259
+ layout='bubble',
260
+ avatar_images=("human.png", "bot.png")
261
+ )
262
+
263
+ error_box = gr.Markdown(visible=False, elem_id="error-box")
264
+
265
+ with gr.Column(scale=2, min_width=300):
266
+ model_dropdown = gr.Dropdown(
267
+ choices=model_choices,
268
+ label="Select Model",
269
+ value="sanjay920/Llama-3-8b-function-calling-alpha-v1"
270
+ )
271
+ model_dropdown.change(load_model, inputs=[model_dropdown])
272
+
273
+ with gr.Accordion("Settings", open=False):
274
+ max_new_tokens = gr.Slider(
275
+ label="Max new tokens",
276
+ minimum=1,
277
+ maximum=MAX_MAX_NEW_TOKENS,
278
+ step=1,
279
+ value=DEFAULT_MAX_NEW_TOKENS,
280
+ )
281
+ temperature = gr.Slider(
282
+ label="Temperature",
283
+ minimum=0.0,
284
+ maximum=1.2,
285
+ step=0.01,
286
+ value=0.01,
287
+ )
288
+
289
+ with gr.Row():
290
+ role = gr.Dropdown(choices=["user", "observation"], value="user", label="Role", scale=4)
291
+ system_prompt = gr.Textbox(label="System Prompt", lines=1, info="Optional")
292
+ tools = gr.Textbox(label="Tools", lines=1, placeholder="Enter tools in JSON format", info="Optional")
293
+
294
+
295
+
296
+ with gr.Row():
297
+ user_input = gr.Textbox(
298
+ label="User Input",
299
+ placeholder="Type your message here...",
300
+ show_label=True,
301
+ scale=8
302
+ )
303
+ submit_btn = gr.Button("Submit", variant="primary", elem_id="submit-button")
304
+ clear_btn = gr.Button("Clear Conversation", elem_id="clear-button")
305
+
306
+
307
+ tools.change(validate_tools, tools, error_box)
308
+
309
+ submit_btn.click(
310
+ user,
311
+ [user_input, chatbot],
312
+ [user_input, chatbot],
313
+ queue=False
314
+ ).then(
315
+ bot,
316
+ [chatbot, system_prompt, tools, role, max_new_tokens, temperature],
317
+ chatbot
318
+ )
319
+
320
+ clear_btn.click(lambda: ([], None), outputs=[chatbot, error_box])
321
+
322
+ gr.Markdown(LICENSE)
323
+
324
+ return demo
325
 
326
  if __name__ == "__main__":
327
+ # Initialize npm project if package.json doesn't exist
328
+ if not os.path.exists('package.json'):
329
+ print("Initializing npm project...")
330
+ run_command("npm init -y")
331
+
332
+ # Install jsonrepair locally
333
+ print("Installing jsonrepair...")
334
+ run_command("npm install jsonrepair")
335
+
336
+ # Verify installation
337
+ print("Verifying jsonrepair installation:")
338
+ run_command("npm list jsonrepair")
339
+
340
+ # Add node_modules/.bin to PATH
341
+ os.environ['PATH'] = f"{os.path.join(os.getcwd(), 'node_modules', '.bin')}:{os.environ['PATH']}"
342
+ from preprocess import preprocess_input
343
+ from postprocess import postprocess_output
344
+ demo = create_chat_interface()
345
+ demo.queue(max_size=20).launch()
install_node.sh ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # installs nvm (Node Version Manager)
3
+ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
4
+
5
+ source ~/.nvm/nvm.sh
6
+
7
+ # download and install Node.js (you may need to restart the terminal)
8
+ nvm install 20
9
+
10
+ # verifies the right Node.js version is in the environment
11
+ ~/.nvm/versions/node/v20.15.0/bin/node -v # should print `v20.15.0`
12
+
13
+ # verifies the right NPM version is in the environment
14
+ ~/.nvm/versions/node/v20.15.0/bin/npm -v # should print `10.7.0`
15
+
16
+ ~/.nvm/versions/node/v20.15.0/bin/npm install jsonrepair -g
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ nodejs
2
+ npm
postprocess.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ import re
4
+ from typing import List
5
+ import subprocess
6
+ import sys
7
+
8
+ def install(package):
9
+ subprocess.check_call([sys.executable, "-m", "pip", "install", package])
10
+
11
+ try:
12
+ import pythonmonkey
13
+ except ImportError:
14
+ install('pythonmonkey')
15
+ import pythonmonkey
16
+
17
+ # Your code using pythonmonkey
18
+
19
+ # Assuming jsonrepair is accessible
20
+ jsonrepair = pythonmonkey.require('jsonrepair').jsonrepair
21
+
22
+ def clean_command_string(command_str):
23
+ cleaned_command = re.sub(r'\\(?!["\\/bfnrt]|u[a-fA-F0-9]{4})', '', command_str)
24
+ cleaned_command = cleaned_command.replace('\\"', '"')
25
+ if cleaned_command.startswith('"') and cleaned_command.endswith('"'):
26
+ cleaned_command = cleaned_command[1:-1]
27
+ return cleaned_command
28
+
29
+ def parse_json_safely(json_str):
30
+ try:
31
+ return json.loads(json_str)
32
+ except json.JSONDecodeError:
33
+ try:
34
+ repaired = jsonrepair(json_str)
35
+ return json.loads(repaired)
36
+ except Exception:
37
+ return json_str
38
+
39
+ def clean_json_object(obj):
40
+ if isinstance(obj, dict):
41
+ return {k: clean_json_object(v) for k, v in obj.items()}
42
+ elif isinstance(obj, list):
43
+ return [clean_json_object(item) for item in obj]
44
+ elif isinstance(obj, str):
45
+ cleaned = clean_command_string(obj)
46
+ return parse_json_safely(cleaned) if cleaned.startswith('{') or cleaned.startswith('[') else cleaned
47
+ else:
48
+ return obj
49
+
50
+ def extract_tool_calls(output_str):
51
+ # Pattern to capture everything after 'starttoolcall' until 'endtoolcall' or end of string if 'endtoolcall' isn't present
52
+ pattern = r'starttoolcall(.*?)(?:endtoolcall|$)'
53
+ matches = [match for match in re.findall(pattern, output_str, re.DOTALL)]
54
+ return matches
55
+
56
+ def extract_tool_calls_and_text(output_str):
57
+ # Initialize an empty list to collect all segments
58
+ segments = []
59
+
60
+ # Last index processed in the string
61
+ last_end = 0
62
+
63
+ # Pattern to capture everything after 'starttoolcall' until 'endtoolcall' or end of string if 'endtoolcall' isn't present
64
+ pattern = r'(starttoolcall(.*?)(?:endtoolcall|$))'
65
+ for match in re.finditer(pattern, output_str, re.DOTALL):
66
+ start, end = match.span(1)
67
+
68
+ # Capture any text between the end of the last tool call and the start of the current one
69
+ if start > last_end:
70
+ text_between = output_str[last_end:start].strip()
71
+ if text_between:
72
+ segments.append({"text": text_between, "type": "text"})
73
+
74
+ # Append the current tool call to the list
75
+ tool_call_content = match.group(2).strip()
76
+ segments.append({"tool_call": tool_call_content, "type": "function"})
77
+
78
+ # Update the last processed index
79
+ last_end = end
80
+
81
+ # Check if there is any remaining text after the last tool call
82
+ if last_end < len(output_str):
83
+ remaining_text = output_str[last_end:].strip()
84
+ if remaining_text:
85
+ segments.append({"text": remaining_text, "type": "text"})
86
+
87
+ return segments
88
+
89
+ def postprocess_output(output_str: str):
90
+ segments = extract_tool_calls_and_text(output_str)
91
+ results = []
92
+
93
+ for segment in segments:
94
+ print("processing segment")
95
+ print(segment)
96
+ if segment['type'] == 'function':
97
+ call = segment['tool_call']
98
+ try:
99
+ parsed_call = parse_json_safely(call)
100
+ cleaned_call = clean_json_object(parsed_call)
101
+
102
+ if isinstance(cleaned_call, dict) and 'name' in cleaned_call and 'arguments' in cleaned_call:
103
+ if isinstance(cleaned_call.get('arguments'), dict):
104
+ cleaned_call['arguments'] = json.dumps(cleaned_call['arguments'])
105
+ results.append({
106
+ "id": uuid.uuid4().hex[:8],
107
+ "function": cleaned_call,
108
+ "type": "function",
109
+ })
110
+ else:
111
+ results.append({
112
+ "id": uuid.uuid4().hex[:8],
113
+ "text": call,
114
+ "type": "text",
115
+ })
116
+ except Exception as e:
117
+ results.append({
118
+ "id": uuid.uuid4().hex[:8],
119
+ "text": call,
120
+ "type": "text",
121
+ })
122
+ else:
123
+ results.append({
124
+ "id": uuid.uuid4().hex[:8],
125
+ "text": segment['text'],
126
+ "type": "text",
127
+ })
128
+
129
+ return results
130
+
131
+ def json_to_markdown(json_obj):
132
+ """Convert a JSON object to a formatted markdown string."""
133
+ markdown = ""
134
+ for item in json_obj:
135
+ if item.get("type") == "text":
136
+ # For text items, just add the text content
137
+ markdown += item.get("text", "") + "\n\n"
138
+ elif item.get("type") == "function":
139
+ # For function calls, format as JSON
140
+ markdown += "```json\n"
141
+ markdown += json.dumps(item.get("function", {}), indent=2)
142
+ markdown += "\n```\n\n"
143
+ return markdown.strip()
144
+
145
+ if __name__ == "__main__":
146
+ # Test the function with a sample input
147
+ # output_str = '''Some text before starttoolcall{"name": "funcA", "arguments": {"param1": 1}endtoolcall
148
+ # More text starttoolcall{"name": "funcB", "arguments": {"param2": "test"}}endtoolcall'''
149
+
150
+ # output_str = '''starttoolcall{"name": "get_current_weather", "arguments": {"location": "San Francisco", "unit": "celsius"}}endtoolcall starttoolcall{"name": "get_current_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}endtoolcall okay great '''
151
+ output_str = '''starttoolcall{"name": "get_current_weather", "arguments": {"location": "San Francisco", "unit": "celsius"}}endtoolcall starttoolcall{"name": "get_current_weather", "arguments": {"location": "Tokyo", "unit": "celsius"}}endtoolcall starttoolcall{"name": "get_current_weather", "arguments": {"location": "Paris", "unit": '''
152
+ parsed_json = postprocess_output(output_str)
153
+ print(json.dumps(parsed_json, indent=2))
154
+
155
+ print("-----")
156
+ print(json_to_markdown(parsed_json))
preprocess.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ import json
3
+
4
+
5
+ TOOL_SYSTEM_PROMPT_RUBRA = (
6
+ "You have access to the following tools: {tool_text}\n"
7
+ "You can choose to respond with one or more tool calls at once, or with a chat message back to the user. "
8
+ "Ensure you have all necessary details before making tool calls. If additional information is needed, "
9
+ "ask the user appropriately. Any tool call you make must correspond to the functions listed above.\n"
10
+ "If you decide to call a tool, format it like this: "
11
+ 'starttoolcall{{"name": "<function_name>", "arguments": {{"<arg1_name>": "<arg1_value>", "<arg2_name>": "<arg2_value>", ...}}}}endtoolcall '
12
+ "where the JSON wrapped between starttoolcall and endtoolcall represents the function call.\n"
13
+ )
14
+
15
+ def json_schema_to_typescript_type(schema, param_name):
16
+ ts_type = "any" # default type
17
+ enum_comment = ""
18
+ integer_comment = ""
19
+ description_comment = ""
20
+
21
+ if isinstance(schema, dict) and "type" in schema:
22
+ json_type = schema["type"]
23
+ if json_type == "array":
24
+ item_type = (
25
+ "any"
26
+ if "items" not in schema
27
+ else json_schema_to_typescript_type(schema["items"], param_name)[0]
28
+ )
29
+ ts_type = f"{item_type}[]"
30
+ elif json_type == "number":
31
+ ts_type = "number"
32
+ elif json_type == "integer":
33
+ ts_type = (
34
+ "number" # TypeScript doesn't differentiate between number and integer
35
+ )
36
+ integer_comment = f" * @param {param_name} - Integer"
37
+ elif json_type == "object":
38
+ ts_type, _ = generate_typescript_interface(schema, param_name)
39
+ elif json_type == "boolean":
40
+ ts_type = "boolean"
41
+ elif json_type == "null":
42
+ ts_type = "null"
43
+ elif json_type == "string":
44
+ ts_type = "string"
45
+
46
+ if "enum" in schema:
47
+ enum_comment = f" * @enum {param_name} - Possible values: " + ", ".join(
48
+ [f'"{enum_value}"' for enum_value in schema["enum"]]
49
+ )
50
+ ts_type = "string"
51
+ if "description" in schema:
52
+ description_comment = f' * @param {param_name} - {schema["description"]}'
53
+
54
+ # Return only the type for nested objects to avoid duplicating comments
55
+ if isinstance(schema, dict) and schema.get("type") == "object":
56
+ return ts_type, "", "", ""
57
+
58
+ return ts_type, enum_comment, integer_comment, description_comment
59
+
60
+
61
+ def generate_typescript_interface(schema, interface_name):
62
+ properties = schema.get("properties", {})
63
+ required = schema.get("required", [])
64
+
65
+ interface_body = []
66
+ descriptions = []
67
+ for prop_name, prop_schema in properties.items():
68
+ prop_type, enum_comment, integer_comment, description_comment = (
69
+ json_schema_to_typescript_type(prop_schema, prop_name)
70
+ )
71
+ is_optional = prop_name not in required
72
+ interface_body.append(
73
+ f' {prop_name}{"?" if is_optional else ""}: {prop_type};'
74
+ )
75
+ if description_comment:
76
+ descriptions.append(description_comment)
77
+ if enum_comment:
78
+ descriptions.append(enum_comment)
79
+ if integer_comment:
80
+ descriptions.append(integer_comment)
81
+
82
+ comments = "\n".join(descriptions)
83
+ interface_definition = (
84
+ f"interface {interface_name} {{\n" + "\n".join(interface_body) + "\n}"
85
+ )
86
+ return interface_definition, comments
87
+
88
+
89
+ def convert_parameters_list_to_dict(parameters):
90
+ properties = {}
91
+ required = []
92
+ for param in parameters:
93
+ properties[param["name"]] = param
94
+ if "default" not in param:
95
+ required.append(param["name"])
96
+ return {"properties": properties, "required": required}
97
+
98
+
99
+ def generate_typescript_function(function_schema) -> str:
100
+ func_name = function_schema["name"]
101
+ description = function_schema.get("description", "")
102
+
103
+ # Check if parameters is a list and convert if necessary
104
+ parameters_info = function_schema.get("parameters", {})
105
+ if isinstance(parameters_info, list):
106
+ parameters_info = convert_parameters_list_to_dict(parameters_info)
107
+ if parameters_info is None:
108
+ parameters_info = {}
109
+
110
+ parameters_schema = parameters_info.get("properties", {})
111
+ required_params = parameters_info.get("required", [])
112
+
113
+ args_list = []
114
+ comments_list = []
115
+ interfaces = []
116
+ for param_name, param_schema in parameters_schema.items():
117
+ ts_type, enum_comment, integer_comment, description_comment = (
118
+ json_schema_to_typescript_type(param_schema, param_name)
119
+ )
120
+ if ts_type.startswith("interface"):
121
+ interface_definition, nested_comments = generate_typescript_interface(
122
+ param_schema, f"{func_name}_{param_name.capitalize()}Params"
123
+ )
124
+ interfaces.append(interface_definition)
125
+ comments_list.append(nested_comments)
126
+ ts_type = f"{func_name}_{param_name.capitalize()}Params"
127
+ else:
128
+ if description_comment:
129
+ comments_list.append(description_comment)
130
+ if enum_comment:
131
+ comments_list.append(enum_comment)
132
+ if integer_comment:
133
+ comments_list.append(integer_comment)
134
+ is_optional = param_name not in required_params
135
+ args_list.append(f'{param_name}{"?" if is_optional else ""}: {ts_type}')
136
+
137
+ args_str = ", ".join(args_list)
138
+ comments_str = "\n".join(comments_list)
139
+ interfaces_str = "\n\n".join(interfaces)
140
+
141
+ description_comment = f" * {description}\n" if description else ""
142
+ typescript_func_declaration = (
143
+ "/**\n"
144
+ + description_comment
145
+ + (comments_str + "\n" if comments_str else "")
146
+ + " */\n"
147
+ + (interfaces_str + "\n\n" if interfaces_str else "")
148
+ + f"function {func_name}({args_str}): any {{}}"
149
+ )
150
+
151
+ return typescript_func_declaration
152
+
153
+
154
+
155
+ def format_tools(tools: List[dict]) -> str:
156
+ func_defs = []
157
+ for t in tools:
158
+ tool_schema = t["function"] if "function" in t else t
159
+ func_defs.append(generate_typescript_function(tool_schema))
160
+
161
+ typescript_functions_str = "\n\n".join(func_defs)
162
+ res = TOOL_SYSTEM_PROMPT_RUBRA.format(tool_text=typescript_functions_str)
163
+ return res
164
+
165
+
166
+
167
+ def preprocess_input(msgs: List[dict], tools: List[dict]):
168
+ tool_system_prompt = format_tools(tools)
169
+ processed_msgs = process_messages(msgs, tool_system_prompt)
170
+ return processed_msgs
171
+
172
+
173
+ def process_messages(messages: List[dict], function_str: str):
174
+ func_observation_map = {}
175
+ processed_msg = []
176
+
177
+ for i in range(len(messages)):
178
+
179
+ if messages[i]["role"] != "tool" and len(func_observation_map) > 0:
180
+ # func_observation_array = [f'{k}: {func_observation_map[k] if func_observation_map[k] != "" else "done"}' for k in func_observation_map]
181
+ func_observation_array = [f'{func_observation_map[k] if func_observation_map[k] != "" else "done"}' for k in func_observation_map]
182
+ observation_str = json.dumps(func_observation_array)
183
+ observation_call = {"role": "user", "content": "start observation " + observation_str + " end observation"}
184
+ processed_msg.append(observation_call)
185
+ func_observation_map.clear()
186
+
187
+ if i == 0:
188
+ if messages[0]["role"] == "system":
189
+ old_content = messages[0]["content"]
190
+ sys_msg = {"role": "system", "content": old_content + "\n" + function_str}
191
+ processed_msg.append(sys_msg)
192
+ else:
193
+ # Insert a system message of tool definition before the first message
194
+ sys_msg = {"role": "system", "content": "You are a helpful assistant.\n" + function_str}
195
+ processed_msg.append(sys_msg)
196
+ processed_msg.append(messages[0]) # first message is always either system or user msg
197
+
198
+ elif messages[i]["role"] == "assistant" and "tool_calls" in messages[i]:
199
+ # Convert OpenAI function call format to Rubra format
200
+ tool_call_str = construct_tool_call_str(messages[i]["tool_calls"], func_observation_map)
201
+ function_call = {"role": "assistant", "content": tool_call_str}
202
+ processed_msg.append(function_call)
203
+
204
+ elif messages[i]["role"] == "tool":
205
+ tool_call_id = messages[i]["tool_call_id"]
206
+ if tool_call_id in func_observation_map:
207
+ func_observation_map[tool_call_id] = messages[i]["content"]
208
+ else:
209
+ print(func_observation_map)
210
+ print(f"Tool call id not found in the map: {tool_call_id}")
211
+ # TODO: the input is not valid in this case, should return an error
212
+
213
+ else:
214
+ processed_msg.append(messages[i])
215
+
216
+
217
+ if len(func_observation_map) > 0:
218
+ # func_observation_array = [f'{k}: {func_observation_map[k] if func_observation_map[k] != "" else "done"}' for k in func_observation_map]
219
+ func_observation_array = [f'{func_observation_map[k] if func_observation_map[k] != "" else "done"}' for k in func_observation_map]
220
+ observation_str = json.dumps(func_observation_array)
221
+ observation_call = {"role": "user", "content": "start observation " + observation_str + " end observation"}
222
+ processed_msg.append(observation_call)
223
+ func_observation_map.clear()
224
+
225
+ return processed_msg
226
+
227
+
228
+ def construct_tool_call_str(tool_calls, func_observation_map) -> str:
229
+ tool_list = []
230
+ for tool_call in tool_calls:
231
+ tool_call_id = tool_call["id"]
232
+ func_observation_map[tool_call_id] = "" # Initialize with empty value, updated later from the message with tool role
233
+
234
+ if type(tool_call["function"]["arguments"]) == str:
235
+ tool_call["function"]["arguments"] = json.loads(tool_call["function"]["arguments"])
236
+ tool_list.append("starttoolcall"+str(tool_call["function"]) + "endtoolcall")
237
+
238
+ # Converting the Python dictionary to a YAML formatted string
239
+ tool_call_str = "".join(tool_list)
240
+ return tool_call_str
241
+
242
+
243
+ if __name__ == "__main__":
244
+ tools = [{
245
+ "type": "function",
246
+ "function": {
247
+ "name": "dummy",
248
+ "description": "just to say hi",
249
+ "parameters": None,
250
+ }
251
+ },{"type": "function","function":{"name":"calculate_distance","description":"Calculate the distance between two locations","parameters":{"type":"object","properties":{"origin":{"type":"string","description":"The starting location"},"destination":{"type":"string","description":"The destination location"},"mode":{"type":"string","description":"The mode of transportation"}},"required":["origin","destination","mode"]}}},{"type": "function","function":{"name":"generate_password","description":"Generate a random password","parameters":{"type":"object","properties":{"length":{"type":"integer","description":"The length of the password"}},"required":["length"]}}}]
252
+ # msgs = [{'role': 'system', 'content': 'You are a helpful assistant.'}, {'role': 'user', 'content': 'What is the distance between San Francisco and Cupertino by driving and by air from both directions?'}, {'role': 'assistant', 'tool_calls': [{'id': '0', 'function': {'name': 'calculate_distance', 'arguments': '{"origin":"San Francisco","destination":"Cupertino","mode":"drive"}'}, 'type': 'function'},{'id': '1', 'function': {'name': 'calculate_distance', 'arguments': '{"origin":"San Francisco","destination":"Cupertino","mode":"air"}'}, 'type': 'function'}]}, {'role': 'tool', 'tool_call_id': '0', 'name': 'calculate_distance', 'content': 'Distance is 50 miles.'}, {'role': 'tool', 'tool_call_id': '1', 'name': 'calculate_distance', 'content': ''}]
253
+ msgs = [{'role': 'user', 'content': "\nYou are task oriented system.\nYou receive input from a user, process the input from the given instructions, and then output the result.\nYour objective is to provide consistent and correct results.\nYou do not need to explain the steps taken, only provide the result to the given instructions.\nYou are referred to as a tool.\nYou don't move to the next step until you have a result.\n\nDownload https://www.sqlitetutorial.net/wp-content/uploads/2018/03/chinook.zip to a\nrandom file. Then expand the archive to a temporary location as there is a sqlite\ndatabase in it.\n\nFirst inspect the schema of the database to understand the table structure.\n\nForm and run a SQL query to find the artist with the most number of albums and output\nthe result of that.\n\nWhen done remove the database file and the downloaded content."}, {'role': 'assistant', 'content': '', 'tool_calls': [{'id': 'cdf7eb2e', 'type': 'function', 'function': {'name': 'download', 'arguments': '{"url":"https://www.sqlitetutorial.net/wp-content/uploads/2018/03/chinook.zip"}'}}]}, {'role': 'tool', 'content': '/tmp/gpt-download2815959664.zip', 'name': 'download', 'tool_call_id': 'cdf7eb2e'}, {'role': 'assistant', 'content': '', 'tool_calls': [{'id': '07cdee86', 'type': 'function', 'function': {'name': 'exec', 'arguments': '{"command":"unzip /tmp/gpt-download2815959664.zip","directory":"."}'}}]}, {'role': 'tool', 'content': 'Archive: /tmp/gpt-download2815959664.zip\n inflating: chinook.db \n', 'name': 'exec', 'tool_call_id': '07cdee86'}, {'role': 'assistant', 'content': '', 'tool_calls': [{'id': 'e8042027', 'type': 'function', 'function': {'name': 'download', 'arguments': '{"url":"https://www.sqlitetutorial.net/wp-content/uploads/2018/03/chinook.zip"}'}}]}, {'role': 'tool', 'content': '/tmp/gpt-download3736408077.zip', 'name': 'download', 'tool_call_id': 'e8042027'}]
254
+ new_msgs = preprocess_input(msgs, tools)
255
+ print(json.dumps(new_msgs, indent=2))
256
+
requirements.txt CHANGED
@@ -1 +1,11 @@
1
- huggingface_hub==0.22.2
 
 
 
 
 
 
 
 
 
 
 
1
+ #!npm install -g npm@latest
2
+ #!npm install -g jsonrepair
3
+ accelerate==0.30.1
4
+ bitsandbytes==0.43.1
5
+ gradio==4.36.1
6
+ scipy==1.13.0
7
+ sentencepiece==0.2.0
8
+ spaces==0.28.3
9
+ torch==2.0.0
10
+ transformers==4.41.0
11
+ pythonmonkey
style.css ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Main container styling */
2
+ .gradio-container {
3
+ height: auto;
4
+ overflow: hidden; /* Prevents overflow outside the main container */
5
+ }
6
+
7
+ /* Main row configuration to fit within the screen */
8
+ #main-row {
9
+ display: flex; /* Ensures that columns within this row are aligned horizontally */
10
+ overflow: hidden; /* Prevents overflow outside the main row */
11
+ }
12
+
13
+ /* Configuration for the column containing the chatbot to ensure it doesn't grow too long */
14
+ .gr-column {
15
+ height: 100%; /* Sets column height to fill the parent container */
16
+ display: flex;
17
+ flex-direction: column;
18
+ overflow: hidden; /* Ensures no overflow outside the column */
19
+ }
20
+
21
+ /* Chatbot specific styling */
22
+ #chatbot {
23
+ flex-grow: 1; /* Allows chatbot to expand within the column space */
24
+ overflow-y: auto; /* Allows vertical scrolling within the chatbot */
25
+ }
26
+
27
+ /* General improvements for component layout and aesthetics */
28
+ .gr-row > .wrap > div, .gr-column > .wrap {
29
+ margin-bottom: 10px;
30
+ }
31
+
32
+ .gr-column {
33
+ gap: 10px;
34
+ align-items: flex-start; /* Aligns items to the start, preventing stretch */
35
+ }