dhisana 0.0.1.dev1__tar.gz → 0.0.1.dev2__tar.gz

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.
Files changed (31) hide show
  1. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/setup.py +1 -1
  3. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/ui/components.py +3 -0
  4. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/agent_tools.py +53 -25
  5. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/assistant_tool_tag.py +1 -1
  6. dhisana-0.0.1.dev2/src/dhisana/utils/openai_helpers.py +485 -0
  7. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/openapi_spec_to_tools.py +5 -3
  8. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana.egg-info/PKG-INFO +1 -1
  9. dhisana-0.0.1.dev1/src/dhisana/utils/openai_helpers.py +0 -272
  10. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/README.md +0 -0
  11. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/pyproject.toml +0 -0
  12. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/setup.cfg +0 -0
  13. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/__init__.py +0 -0
  14. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/cli/__init__.py +0 -0
  15. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/cli/cli.py +0 -0
  16. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/cli/datasets.py +0 -0
  17. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/cli/models.py +0 -0
  18. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/cli/predictions.py +0 -0
  19. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/ui/__init__.py +0 -0
  20. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/__init__.py +0 -0
  21. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  22. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  23. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  24. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  25. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana/utils/tools_json.py +0 -0
  26. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana.egg-info/SOURCES.txt +0 -0
  27. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana.egg-info/dependency_links.txt +0 -0
  28. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana.egg-info/entry_points.txt +0 -0
  29. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana.egg-info/requires.txt +0 -0
  30. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/src/dhisana.egg-info/top_level.txt +0 -0
  31. {dhisana-0.0.1.dev1 → dhisana-0.0.1.dev2}/tests/test_agent_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dhisana
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.1.dev2
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='dhisana',
5
- version='0.0.1-dev1',
5
+ version='0.0.1-dev2',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -224,11 +224,13 @@ class Tab(Component):
224
224
  class ModalDialog(Component):
225
225
  def __init__(
226
226
  self,
227
+ name: str,
227
228
  title: str,
228
229
  content: List[Component],
229
230
  visible: bool = False,
230
231
  on_close: Optional[str] = None,
231
232
  ):
233
+ self.name = name
232
234
  self.title = title
233
235
  self.content = content
234
236
  self.visible = visible
@@ -238,6 +240,7 @@ class ModalDialog(Component):
238
240
  return {
239
241
  'type': 'modal-dialog',
240
242
  'properties': {
243
+ 'name': self.name,
241
244
  'title': self.title,
242
245
  'visible': self.visible,
243
246
  'onClose': self.on_close,
@@ -23,12 +23,9 @@ from googleapiclient.errors import HttpError
23
23
  GLOBAL_DATA_MODELS = []
24
24
  GLOBAL_TOOLS_FUNCTIONS = {}
25
25
 
26
+
26
27
  @assistant_tool
27
28
  async def get_html_content_from_url(url):
28
- # Check and replace http with https
29
- if url.startswith("http://"):
30
- url = url.replace("http://", "https://", 1)
31
-
32
29
  async with async_playwright() as playwright:
33
30
  browser = await playwright.chromium.launch(headless=True)
34
31
  context = await browser.new_context()
@@ -45,6 +42,7 @@ async def get_html_content_from_url(url):
45
42
  finally:
46
43
  await browser.close()
47
44
 
45
+
48
46
  async def parse_html_content(html_content):
49
47
  if not html_content:
50
48
  return ""
@@ -53,6 +51,7 @@ async def parse_html_content(html_content):
53
51
  element.decompose()
54
52
  return soup.get_text(separator=' ', strip=True)
55
53
 
54
+
56
55
  def convert_base_64_json(base64_string):
57
56
  """
58
57
  Convert a base64 encoded string to a JSON string.
@@ -65,12 +64,13 @@ def convert_base_64_json(base64_string):
65
64
  """
66
65
  # Decode the base64 string to bytes
67
66
  decoded_bytes = base64.b64decode(base64_string)
68
-
67
+
69
68
  # Convert bytes to JSON string
70
69
  json_string = decoded_bytes.decode('utf-8')
71
-
70
+
72
71
  return json_string
73
72
 
73
+
74
74
  @assistant_tool
75
75
  async def get_file_content_from_googledrive_by_name(file_name: str = None) -> str:
76
76
  """
@@ -101,12 +101,13 @@ async def get_file_content_from_googledrive_by_name(file_name: str = None) -> st
101
101
 
102
102
  # Search for the file by name
103
103
  query = f"name = '{file_name}'"
104
- results = service.files().list(q=query, pageSize=1, fields="files(id, name)").execute()
104
+ results = service.files().list(q=query, pageSize=1,
105
+ fields="files(id, name)").execute()
105
106
  items = results.get('files', [])
106
107
 
107
108
  if not items:
108
109
  raise FileNotFoundError(f"No file found with the name: {file_name}")
109
-
110
+
110
111
  # Get the file ID of the first matching file
111
112
  file_id = items[0]['id']
112
113
  file_name = items[0]['name']
@@ -119,7 +120,7 @@ async def get_file_content_from_googledrive_by_name(file_name: str = None) -> st
119
120
 
120
121
  # Request the file content from Google Drive
121
122
  request = service.files().get_media(fileId=file_id)
122
-
123
+
123
124
  # Create a file-like object in memory to hold the downloaded data
124
125
  fh = io.FileIO(local_file_path, 'wb')
125
126
 
@@ -137,14 +138,15 @@ async def get_file_content_from_googledrive_by_name(file_name: str = None) -> st
137
138
  # Return the local file path
138
139
  return local_file_path
139
140
 
141
+
140
142
  @assistant_tool
141
- async def write_content_to_googledrive(cloud_file_name: str, local_file_name: str) -> str:
143
+ async def write_content_to_googledrive(cloud_file_path: str, local_file_path: str) -> str:
142
144
  """
143
145
  Writes content from a local file to a file in Google Drive using a service account.
144
- If the file does not exist in Google Drive, it creates it.
146
+ If the file does not exist in Google Drive, it creates it along with any necessary intermediate directories.
145
147
 
146
- :param cloud_file_name: The name of the file to create or update on Google Drive.
147
- :param local_file_name: The path to the local file whose content will be uploaded.
148
+ :param cloud_file_path: The path of the file to create or update on Google Drive.
149
+ :param local_file_path: The path to the local file whose content will be uploaded.
148
150
  :return: The file ID of the uploaded or updated file.
149
151
  """
150
152
 
@@ -167,13 +169,35 @@ async def write_content_to_googledrive(cloud_file_name: str, local_file_name: st
167
169
  # Build the Google Drive service object
168
170
  service = build('drive', 'v3', credentials=credentials)
169
171
 
170
- # Check if the file exists in Google Drive
171
- query = f"name = '{cloud_file_name}'"
172
- results = service.files().list(q=query, pageSize=1, fields="files(id, name)").execute()
173
- items = results.get('files', [])
172
+ # Split the cloud file path into components
173
+ path_components = cloud_file_path.split('/')
174
+ parent_id = 'root'
175
+
176
+ # Create intermediate directories if they don't exist
177
+ for component in path_components[:-1]:
178
+ query = f"'{parent_id}' in parents and name = '{component}' and mimeType = 'application/vnd.google-apps.folder'"
179
+ results = service.files().list(q=query, pageSize=1, fields="files(id, name)").execute()
180
+ items = results.get('files', [])
181
+
182
+ if items:
183
+ parent_id = items[0]['id']
184
+ else:
185
+ file_metadata = {
186
+ 'name': component,
187
+ 'mimeType': 'application/vnd.google-apps.folder',
188
+ 'parents': [parent_id]
189
+ }
190
+ folder = service.files().create(body=file_metadata, fields='id').execute()
191
+ parent_id = folder.get('id')
174
192
 
175
193
  # Prepare the file for upload
176
- media_body = MediaFileUpload(local_file_name, resumable=True)
194
+ media_body = MediaFileUpload(local_file_path, resumable=True)
195
+ file_name = path_components[-1]
196
+
197
+ # Check if the file exists in the specified directory
198
+ query = f"'{parent_id}' in parents and name = '{file_name}'"
199
+ results = service.files().list(q=query, pageSize=1, fields="files(id, name)").execute()
200
+ items = results.get('files', [])
177
201
 
178
202
  if items:
179
203
  # File exists, update its content
@@ -184,7 +208,10 @@ async def write_content_to_googledrive(cloud_file_name: str, local_file_name: st
184
208
  ).execute()
185
209
  else:
186
210
  # File does not exist, create a new one
187
- file_metadata = {'name': cloud_file_name}
211
+ file_metadata = {
212
+ 'name': file_name,
213
+ 'parents': [parent_id]
214
+ }
188
215
  created_file = service.files().create(
189
216
  body=file_metadata,
190
217
  media_body=media_body,
@@ -194,8 +221,6 @@ async def write_content_to_googledrive(cloud_file_name: str, local_file_name: st
194
221
 
195
222
  return file_id
196
223
 
197
-
198
-
199
224
  @assistant_tool
200
225
  async def list_files_in_drive_folder_by_name(folder_path: str = None) -> List[str]:
201
226
  """
@@ -230,11 +255,13 @@ async def list_files_in_drive_folder_by_name(folder_path: str = None) -> List[st
230
255
 
231
256
  if folder_path:
232
257
  # Split the folder path into individual folder names
233
- folder_names = [name for name in folder_path.strip('/').split('/') if name]
258
+ folder_names = [name for name in folder_path.strip(
259
+ '/').split('/') if name]
234
260
  for folder_name in folder_names:
235
261
  # Search for the folder by name under the current folder_id
236
262
  query = (
237
- f"name = '{folder_name}' and mimeType = 'application/vnd.google-apps.folder' "
263
+ f"name = '{
264
+ folder_name}' and mimeType = 'application/vnd.google-apps.folder' "
238
265
  f"and '{folder_id}' in parents and trashed = false"
239
266
  )
240
267
  try:
@@ -246,7 +273,7 @@ async def list_files_in_drive_folder_by_name(folder_path: str = None) -> List[st
246
273
  items = results.get('files', [])
247
274
  if not items:
248
275
  raise FileNotFoundError(
249
- f"Folder '{folder_name}' not found under parent folder ID '{folder_id}'"
276
+ f"Folder '{folder_name}' not found under parent folder ID '{folder_id}'"
250
277
  )
251
278
  # Update folder_id to the ID of the found folder
252
279
  folder_id = items[0]['id']
@@ -424,4 +451,5 @@ async def get_calendar_events_using_service_account_async(
424
451
 
425
452
  return events
426
453
 
427
- GLOBAL_TOOLS_FUNCTIONS = {name: func for name, func in globals().items() if callable(func) and getattr(func, 'is_assistant_tool', False)}
454
+ GLOBAL_TOOLS_FUNCTIONS = {name: func for name, func in globals().items(
455
+ ) if callable(func) and getattr(func, 'is_assistant_tool', False)}
@@ -1,4 +1,4 @@
1
1
  # Read API keys from environment variables
2
2
  def assistant_tool(func):
3
3
  func.is_assistant_tool = True
4
- return func
4
+ return func
@@ -0,0 +1,485 @@
1
+ # Helper functions to call OpenAI Assistant
2
+
3
+ from datetime import datetime
4
+ import json
5
+ import asyncio
6
+ from typing import Dict, List
7
+ import logging
8
+ import os
9
+
10
+ from openai import OpenAI
11
+ from pydantic import BaseModel
12
+ from fastapi import HTTPException
13
+ from openai import LengthFinishReasonError, OpenAI, OpenAIError
14
+ import csv
15
+
16
+
17
+ from .agent_tools import GLOBAL_DATA_MODELS, GLOBAL_TOOLS_FUNCTIONS, get_file_content_from_googledrive_by_name, write_content_to_googledrive
18
+ from .tools_json import GLOBAL_ASSISTANT_TOOLS
19
+ from .openapi_spec_to_tools import (
20
+ OPENAPI_TOOL_CONFIGURATIONS,
21
+ OPENAPI_GLOBAL_ASSISTANT_TOOLS,
22
+ OPENAPI_CALLABALE_FUNCTIONS,
23
+ )
24
+
25
+
26
+ async def read_from_google_drive(path):
27
+ return await get_file_content_from_googledrive_by_name(file_name=path)
28
+
29
+ # Function to get headers for OpenAPI tools
30
+ def get_headers(toolname):
31
+ headers = OPENAPI_TOOL_CONFIGURATIONS.get(toolname, {}).get("headers", {})
32
+ return headers
33
+
34
+
35
+ def get_params(toolname):
36
+ params = OPENAPI_TOOL_CONFIGURATIONS.get(toolname, {}).get("params", {})
37
+ return params
38
+
39
+
40
+ async def run_assistant(client, assistant, thread, prompt, response_type, allowed_tools):
41
+ """
42
+ Runs the assistant with the given parameters.
43
+ """
44
+ await send_initial_message(client, thread, prompt)
45
+ allowed_tool_items = get_allowed_tool_items(allowed_tools)
46
+ response_format = get_response_format(response_type)
47
+
48
+ run = client.beta.threads.runs.create_and_poll(
49
+ thread_id=thread.id,
50
+ assistant_id=assistant.id,
51
+ response_format=response_format,
52
+ tools=allowed_tool_items,
53
+ )
54
+
55
+ max_iterations = 5
56
+ iteration_count = 0
57
+
58
+ while run.status == 'requires_action':
59
+ if iteration_count >= max_iterations:
60
+ print("Exceeded maximum number of iterations for requires_action.")
61
+ return "Error: Exceeded maximum number of iterations for requires_action."
62
+
63
+ tool_outputs = await handle_required_action(run)
64
+ if tool_outputs:
65
+ run = await submit_tool_outputs(client, thread, run, tool_outputs)
66
+
67
+ iteration_count += 1
68
+
69
+ return await handle_run_completion(client, thread, run)
70
+
71
+
72
+ async def send_initial_message(client, thread, prompt):
73
+ client.beta.threads.messages.create(
74
+ thread_id=thread.id,
75
+ role="user",
76
+ content=prompt,
77
+ )
78
+
79
+
80
+ def get_allowed_tool_items(allowed_tools):
81
+ allowed_tool_items = [
82
+ tool for tool in GLOBAL_ASSISTANT_TOOLS
83
+ if tool['type'] == 'function' and tool['function']['name'] in allowed_tools
84
+ ]
85
+ allowed_tool_items.extend([
86
+ tool for tool in OPENAPI_GLOBAL_ASSISTANT_TOOLS
87
+ if tool['type'] == 'function' and tool['function']['name'] in allowed_tools
88
+ ])
89
+ return allowed_tool_items
90
+
91
+
92
+ def get_response_format(response_type):
93
+ return {
94
+ 'type': 'json_schema',
95
+ 'json_schema': {
96
+ "name": response_type.__name__,
97
+ "schema": response_type.model_json_schema()
98
+ }
99
+ }
100
+
101
+
102
+ async def handle_required_action(run):
103
+ tool_outputs = []
104
+ current_batch_size = 0
105
+ max_batch_size = 256 * 1024 # 256 KB
106
+
107
+ if hasattr(run, 'required_action') and hasattr(run.required_action, 'submit_tool_outputs'):
108
+ for tool in run.required_action.submit_tool_outputs.tool_calls:
109
+ function, openai_function = get_function(tool.function.name)
110
+ if function:
111
+ output_str, output_size = await invoke_function(function, tool, openai_function)
112
+ if current_batch_size + output_size > max_batch_size:
113
+ tool_outputs.append(
114
+ {"tool_call_id": tool.id, "output": ""})
115
+ else:
116
+ tool_outputs.append(
117
+ {"tool_call_id": tool.id, "output": output_str})
118
+ current_batch_size += output_size
119
+ else:
120
+ print(f"Function {tool.function.name} not found.")
121
+ tool_outputs.append(
122
+ {"tool_call_id": tool.id, "output": "No results found"})
123
+
124
+ return tool_outputs
125
+
126
+
127
+ def get_function(function_name):
128
+ function = GLOBAL_TOOLS_FUNCTIONS.get(function_name)
129
+ openai_function = False
130
+ if not function:
131
+ function = OPENAPI_CALLABALE_FUNCTIONS.get(function_name)
132
+ openai_function = True
133
+ return function, openai_function
134
+
135
+
136
+ async def invoke_function(function, tool, openai_function):
137
+ try:
138
+ function_args = json.loads(tool.function.arguments)
139
+ print(f"Invoking function {tool.function.name} with args: {function_args}\n")
140
+
141
+ if openai_function:
142
+ output = await invoke_openai_function(function, function_args, tool.function.name)
143
+ else:
144
+ if asyncio.iscoroutinefunction(function):
145
+ output = await function(**function_args)
146
+ else:
147
+ output = function(**function_args)
148
+ output_str = json.dumps(output)
149
+ output_size = len(output_str.encode('utf-8'))
150
+ print(f"\nOutput from function {tool.function.name}: {output_str[:256]}\n")
151
+
152
+ return output_str, output_size
153
+ except Exception as e:
154
+ print(f"Error invoking function {tool.function.name}: {e}")
155
+ return "No results found", 0
156
+
157
+
158
+ async def invoke_openai_function(function, function_args, function_name):
159
+
160
+ json_body = function_args.get("json", None)
161
+ path_params = function_args.get("path_params", None)
162
+ fn_args = {"path_params": path_params, "data": json_body}
163
+ headers = get_headers(function_name)
164
+
165
+ query_params = function_args.get("params", {})
166
+ params = get_params(function_name)
167
+ query_params.update(params)
168
+ if asyncio.iscoroutinefunction(function):
169
+ output_fn = await function(
170
+ name=function_name,
171
+ fn_args=fn_args,
172
+ headers=headers,
173
+ params=query_params,
174
+ )
175
+ else:
176
+ output_fn = function(
177
+ name=function_name,
178
+ fn_args=fn_args,
179
+ headers=headers,
180
+ params=query_params,
181
+ )
182
+ print(f"\nOutput from function {function_name}: {output_fn.status_code} {output_fn.reason}\n")
183
+ return {
184
+ "status_code": output_fn.status_code,
185
+ "text": output_fn.text,
186
+ "reason": output_fn.reason,
187
+ }
188
+
189
+
190
+ async def submit_tool_outputs(client, thread, run, tool_outputs):
191
+ try:
192
+ return client.beta.threads.runs.submit_tool_outputs_and_poll(
193
+ thread_id=thread.id,
194
+ run_id=run.id,
195
+ tool_outputs=tool_outputs
196
+ )
197
+ except Exception as e:
198
+ print("Failed to submit tool outputs:", e)
199
+ return run
200
+
201
+
202
+ async def handle_run_completion(client, thread, run):
203
+ if run.status == 'completed':
204
+ messages = client.beta.threads.messages.list(thread_id=thread.id)
205
+ return messages.data[0].content[0].text.value
206
+ else:
207
+ print("Failed to run assistant:", run.status)
208
+ return run.status
209
+
210
+
211
+ async def extract_and_structure_data(client, assistant, thread, prompt, task_inputs, response_type, allowed_tools):
212
+ # Replace placeholders in the prompt with task inputs
213
+ formatted_prompt = prompt
214
+ for key, value in task_inputs.items():
215
+ placeholder = "{{ inputs." + key + " }}"
216
+ formatted_prompt = formatted_prompt.replace(placeholder, str(value))
217
+ output = await run_assistant(client, assistant, thread, formatted_prompt, response_type, allowed_tools)
218
+ return output
219
+
220
+ class RowItem(BaseModel):
221
+ column_value: str
222
+
223
+ class ResponseList(BaseModel):
224
+ rows: List[RowItem]
225
+
226
+ def lookup_response_type(name: str):
227
+ for model in GLOBAL_DATA_MODELS:
228
+ if model.__name__ == name:
229
+ return model
230
+ return ResponseList # Default response type
231
+
232
+
233
+ async def process_agent_request(row_batch: List[Dict], workflow: Dict, custom_instructions: str) -> List[Dict]:
234
+ """
235
+ Process agent request using the OpenAI client.
236
+ """
237
+ try:
238
+ client = OpenAI()
239
+ assistant = client.beta.assistants.create(
240
+ name="AI Assistant",
241
+ instructions=f"Hi, You are an AI Assistant. Help the user with their tasks.\n\n{custom_instructions}\n\n",
242
+ tools=[],
243
+ model="gpt-4o-2024-08-06"
244
+ )
245
+ thread = client.beta.threads.create()
246
+
247
+ parsed_outputs = []
248
+ for row in row_batch:
249
+ try:
250
+ task_outputs = {} # Dictionary to store outputs of tasks
251
+ input_list = {}
252
+ input_list['initial_input_list'] = {
253
+ "data": [row],
254
+ "format": "list"
255
+ }
256
+ task_outputs['initial_input'] = input_list
257
+ for task in workflow['tasks']:
258
+ # Process each task
259
+ task_outputs = await process_task(client, assistant, thread, row, task, task_outputs)
260
+ # Collect the final output
261
+ parsed_outputs.append(task_outputs)
262
+ except Exception as e:
263
+ print(f"Error processing row {row}: {e}")
264
+ return parsed_outputs
265
+ except Exception as e:
266
+ print(f"An error occurred: {e}")
267
+ return "Error Processing Leads"
268
+ finally:
269
+ try:
270
+ client.beta.assistants.delete(assistant.id)
271
+ except Exception as e:
272
+ print(f"Error deleting assistant: {e}")
273
+
274
+
275
+ async def process_task(client, assistant, thread, row, task, task_outputs):
276
+ """
277
+ Process a single task in the workflow.
278
+ """
279
+ try:
280
+ # Prepare inputs
281
+ task_inputs = await prepare_task_inputs(row, task, task_outputs)
282
+
283
+ # Run the operation
284
+ output = await run_task_operation(client, assistant, thread, task, task_inputs)
285
+
286
+ # Store outputs
287
+ await store_task_outputs(task, output, task_outputs)
288
+
289
+ return task_outputs
290
+ except Exception as e:
291
+ print(f"Error processing task {task['id']}: {e}")
292
+ return task_outputs
293
+
294
+ async def read_csv_rows(file_path):
295
+ rows = []
296
+ with open(file_path, mode='r') as file:
297
+ csv_reader = csv.reader(file)
298
+ for row in csv_reader:
299
+ rows.append(row)
300
+ return rows
301
+
302
+ async def prepare_task_inputs(row, task, task_outputs):
303
+ """
304
+ Prepare the inputs for a task based on its input specifications.
305
+ """
306
+ inputs = task.get('inputs', {})
307
+ task_inputs = {}
308
+ for input_name, input_spec in inputs.items():
309
+ source = input_spec.get('source', {})
310
+ source_type = source.get('type', '')
311
+ format = input_spec.get('format', 'list')
312
+ if source_type == 'external':
313
+ # External source, get from initial input
314
+ input_data = row.get(input_name, row)
315
+ elif source_type == 'task_output':
316
+ # Get from previous task output
317
+ task_id = source.get('task_id')
318
+ output_key = source.get('output_key')
319
+ previous_task_output = task_outputs.get(task_id, {})
320
+ print(f"Previous task output: {previous_task_output} Output key: {output_key}")
321
+ if isinstance(previous_task_output, dict):
322
+ output_item = previous_task_output.get(output_key)
323
+ input_data = output_item['data']
324
+ else:
325
+ input_data = previous_task_output
326
+
327
+ # Ensure input_data is a list
328
+ if not isinstance(input_data, list):
329
+ input_data = [input_data]
330
+ elif source_type == 'google_drive':
331
+ # Handle Google Drive source
332
+ path = source.get('location')
333
+ input_data_path = await read_from_google_drive(path)
334
+ input_data = await read_csv_rows(input_data_path)
335
+ elif source_type == 'local_path':
336
+ # Handle local path source
337
+ input_data_path = source.get('location')
338
+ input_data = await read_csv_rows(input_data_path)
339
+ else:
340
+ input_data = None
341
+ if input_data:
342
+ task_inputs[input_name] = {
343
+ "format": format,
344
+ "data" : input_data
345
+ }
346
+ return task_inputs
347
+
348
+ async def run_task_operation(client, assistant, thread, task, task_inputs):
349
+ """
350
+ Execute the operation defined in the task.
351
+ """
352
+ operation = task.get('operation', {})
353
+ operation_type = operation.get('type', '')
354
+ allowed_tools = operation.get('allowed_tools', [])
355
+ response_type_name = operation.get('response_type', 'ResponseList')
356
+ response_type = lookup_response_type(response_type_name)
357
+ outputs = []
358
+
359
+ if operation_type == 'ai_assistant_call':
360
+ prompt_template = operation.get('prompt', '')
361
+ args = operation.get('args', [])
362
+ # Prepare prompt by substituting inputs
363
+
364
+ formatted_prompt = prompt_template
365
+ for key, value in task_inputs.items():
366
+ format = value.get('format', 'list')
367
+ if format == 'list':
368
+ for item in value.get('data'):
369
+ formatted_prompt = formatted_prompt.replace(
370
+ "{{ inputs." + key + " }}", json.dumps(item))
371
+ # Run assistant with prompt
372
+ output = await extract_and_structure_data(
373
+ client, assistant, thread, formatted_prompt, task_inputs, response_type, allowed_tools)
374
+ outputs.append(output)
375
+ else:
376
+ pass # TODO: Handle other formats
377
+ elif operation_type == 'python_callable':
378
+ function_name = operation.get('function', '')
379
+ args = operation.get('args', [])
380
+ function = globals().get(function_name)
381
+ if function is None:
382
+ raise Exception(f"Function {function_name} not found.")
383
+ # Prepare function arguments
384
+ function_args = [task_inputs.get(arg) for arg in args]
385
+ # Call the function
386
+ if asyncio.iscoroutinefunction(function):
387
+ output = await function(*function_args)
388
+ else:
389
+ output = function(*function_args)
390
+ outputs.append(output)
391
+ else:
392
+ # Handle other operation types
393
+ output = None
394
+ return_val = {
395
+ "data": outputs,
396
+ "format": "list"
397
+ }
398
+ return return_val
399
+
400
+ async def store_task_outputs(task, output, task_outputs):
401
+ """
402
+ Store the outputs of a task for use in subsequent tasks.
403
+ """
404
+ outputs = task.get('outputs', {})
405
+ if outputs:
406
+ for output_name, output_spec in outputs.items():
407
+ # Store output in task_outputs using task id and output_name
408
+ if task['id'] not in task_outputs:
409
+ task_outputs[task['id']] = {}
410
+
411
+ destination = output_spec.get('destination', {})
412
+ if destination:
413
+ dest_type = destination.get('type')
414
+ path_template = destination.get('path_template')
415
+ if path_template:
416
+ current_timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
417
+ path = path_template.replace('{timestamp}', current_timestamp)
418
+ path = path.replace('{task_id}', task['id'])
419
+ local_path = path
420
+
421
+ if dest_type == 'google_drive':
422
+ local_path = os.path.join('/tmp', path)
423
+
424
+ if dest_type == 'google_drive' or dest_type == 'local_path':
425
+ directory = os.path.dirname(local_path)
426
+ if directory and not os.path.exists(directory):
427
+ os.makedirs(directory)
428
+ with open(local_path, 'w') as file:
429
+ if output.get("format", "") == 'list':
430
+ data_list = [json.loads(item) for item in output.get("data", [])]
431
+ if data_list:
432
+ # Filter headers to include only simple types
433
+ headers = [key for key, value in data_list[0].items() if isinstance(value, (str, int, float, bool))]
434
+ writer = csv.DictWriter(file, fieldnames=headers)
435
+ writer.writeheader()
436
+ for data in data_list:
437
+ filtered_data = {key: value for key, value in data.items() if key in headers}
438
+ writer.writerow(filtered_data)
439
+ else:
440
+ file.write(str(output))
441
+ else:
442
+ # Ignore if destination type is not google_drive or local_path
443
+ pass
444
+
445
+ if dest_type == 'google_drive':
446
+ await write_to_google_drive(path, local_path)
447
+
448
+ task_outputs[task['id']][output_name] = output
449
+ else:
450
+ # If no outputs are defined, store the output under the task id
451
+ task_outputs[task['id']] = output
452
+
453
+ async def write_to_google_drive(cloud_path, local_path):
454
+ # Placeholder function for writing to Google Drive
455
+ await write_content_to_googledrive(cloud_path, local_path)
456
+ print(f"Writing to Google Drive at {cloud_path} {local_path}")
457
+
458
+ def get_structured_output(message: str, response_type):
459
+ try:
460
+ client = OpenAI()
461
+ completion = client.beta.chat.completions.parse(
462
+ model="gpt-4o-2024-08-06",
463
+ messages=[
464
+ {"role": "system", "content": "Extract structured content from input. Output is in JSON Format."},
465
+ {"role": "user", "content": message},
466
+ ],
467
+ response_format=response_type,
468
+ )
469
+
470
+ response = completion.choices[0].message
471
+ if response.parsed:
472
+ return response.parsed, 'SUCCESS'
473
+ elif response.refusal:
474
+ logging.warning("ERROR: Refusal response: %s", response.refusal)
475
+ return response.refusal, 'FAIL'
476
+
477
+ except LengthFinishReasonError as e:
478
+ logging.error(f"Too many tokens: {e}")
479
+ raise HTTPException(status_code=502, detail="The request exceeded the maximum token limit.")
480
+ except OpenAIError as e:
481
+ logging.error(f"OpenAI API error: {e}")
482
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
483
+ except Exception as e:
484
+ logging.error(f"Unexpected error: {e}")
485
+ raise HTTPException(status_code=500, detail="An unexpected error occurred while processing your request.")
@@ -12,6 +12,7 @@ OPENAPI_GLOBAL_ASSISTANT_TOOLS = []
12
12
  OPENAPI_CALLABALE_FUNCTIONS = {}
13
13
  OPENAPI_TOOL_CONFIGURATIONS = {}
14
14
 
15
+
15
16
  def convert_spec_to_tools(file_path: str):
16
17
  # Open the file and load spec from there
17
18
  with open(file_path, 'r') as file:
@@ -20,7 +21,9 @@ def convert_spec_to_tools(file_path: str):
20
21
  openai_fns, call_api_fn = openapi_spec_to_openai_fn(spec)
21
22
  return openai_fns, call_api_fn
22
23
 
23
- # Parse and save the OpenAI Tools parsed and the corresponding callable functions
24
+ # Parse and save the OpenAI Tools parsed and the corresponding callable functions
25
+
26
+
24
27
  def add_openapi_spec_to_tools_list(file_path: str):
25
28
  openai_fns, call_api_fn = convert_spec_to_tools(file_path)
26
29
  if (len(openai_fns) > 0):
@@ -28,6 +31,5 @@ def add_openapi_spec_to_tools_list(file_path: str):
28
31
  for fn in openai_fns:
29
32
  name = fn["function"]["name"]
30
33
  if name:
31
- OPENAPI_CALLABALE_FUNCTIONS[name]= call_api_fn
34
+ OPENAPI_CALLABALE_FUNCTIONS[name] = call_api_fn
32
35
  return OPENAPI_GLOBAL_ASSISTANT_TOOLS
33
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dhisana
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.1.dev2
4
4
  Summary: A Python SDK for Dhisana AI Platform
5
5
  Home-page: https://github.com/dhisana-ai/dhisana-python-sdk
6
6
  Author: Admin
@@ -1,272 +0,0 @@
1
- # Helper functions to call OpenAI Assistant
2
-
3
- import logging
4
- import os
5
- import json
6
- from typing import Dict, List
7
-
8
- from fastapi import HTTPException
9
- from openai import LengthFinishReasonError, OpenAI, OpenAIError
10
- from pydantic import BaseModel
11
-
12
- from dhisana.utils.agent_tools import GLOBAL_DATA_MODELS, GLOBAL_TOOLS_FUNCTIONS
13
- from .tools_json import GLOBAL_ASSISTANT_TOOLS
14
- from .openapi_spec_to_tools import OPENAPI_TOOL_CONFIGURATIONS, OPENAPI_GLOBAL_ASSISTANT_TOOLS, OPENAPI_CALLABALE_FUNCTIONS
15
-
16
- def get_headers(toolname):
17
- headers = OPENAPI_TOOL_CONFIGURATIONS.get(toolname, {}).get("headers", {})
18
- return headers
19
-
20
- def get_params(toolname):
21
- headers = OPENAPI_TOOL_CONFIGURATIONS.get(toolname, {}).get("params", {})
22
- return headers
23
-
24
- async def run_assistant(client, assistant, thread, prompt, response_type, allowed_tools):
25
- """
26
- Runs the assistant with the given parameters.
27
- """
28
- send_initial_message(client, thread, prompt)
29
- allowed_tool_items = get_allowed_tool_items(allowed_tools)
30
- response_format = get_response_format(response_type)
31
-
32
- run = client.beta.threads.runs.create_and_poll(
33
- thread_id=thread.id,
34
- assistant_id=assistant.id,
35
- response_format=response_format,
36
- tools=allowed_tool_items,
37
- )
38
-
39
- max_iterations = 5
40
- iteration_count = 0
41
-
42
- while run.status == 'requires_action':
43
- if iteration_count >= max_iterations:
44
- print("Exceeded maximum number of iterations for requires_action.")
45
- return "Error: Exceeded maximum number of iterations for requires_action."
46
-
47
- tool_outputs = await handle_required_action(run)
48
- if tool_outputs:
49
- run = submit_tool_outputs(client, thread, run, tool_outputs)
50
-
51
- iteration_count += 1
52
-
53
- return handle_run_completion(client, thread, run)
54
-
55
-
56
- def send_initial_message(client, thread, prompt):
57
- client.beta.threads.messages.create(
58
- thread_id=thread.id,
59
- role="user",
60
- content=prompt,
61
- )
62
-
63
-
64
- def get_allowed_tool_items(allowed_tools):
65
- allowed_tool_items = [
66
- tool for tool in GLOBAL_ASSISTANT_TOOLS
67
- if tool['type'] == 'function' and tool['function']['name'] in allowed_tools
68
- ]
69
- allowed_tool_items.extend([
70
- tool for tool in OPENAPI_GLOBAL_ASSISTANT_TOOLS
71
- if tool['type'] == 'function' and tool['function']['name'] in allowed_tools
72
- ])
73
- return allowed_tool_items
74
-
75
-
76
- def get_response_format(response_type):
77
- return {
78
- 'type': 'json_schema',
79
- 'json_schema': {
80
- "name": response_type.__class__.__name__,
81
- "schema": response_type.model_json_schema()
82
- }
83
- }
84
-
85
-
86
- async def handle_required_action(run):
87
- tool_outputs = []
88
- current_batch_size = 0
89
- max_batch_size = 256 * 1024
90
-
91
- if hasattr(run, 'required_action') and hasattr(run.required_action, 'submit_tool_outputs'):
92
- for tool in run.required_action.submit_tool_outputs.tool_calls:
93
- function, openai_function = get_function(tool.function.name)
94
- if function:
95
- output_str, output_size = await invoke_function(function, tool, openai_function)
96
- if current_batch_size + output_size > max_batch_size:
97
- tool_outputs.append({"tool_call_id": tool.id, "output": ""})
98
- else:
99
- tool_outputs.append({"tool_call_id": tool.id, "output": output_str})
100
- current_batch_size += output_size
101
- else:
102
- print(f"Function {tool.function.name} not found.")
103
- tool_outputs.append({"tool_call_id": tool.id, "output": "No results found"})
104
-
105
- return tool_outputs
106
-
107
-
108
- def get_function(function_name):
109
- function = GLOBAL_TOOLS_FUNCTIONS.get(function_name)
110
- openai_function = False
111
- if not function:
112
- function = OPENAPI_CALLABALE_FUNCTIONS.get(function_name)
113
- openai_function = True
114
- return function, openai_function
115
-
116
-
117
- async def invoke_function(function, tool, openai_function):
118
- try:
119
- function_args = json.loads(tool.function.arguments)
120
- print(f"Invoking function {tool.function.name} with args: {function_args}\n")
121
- if openai_function:
122
- output = invoke_openai_function(function, function_args, tool.function.name)
123
- else:
124
- output = await function(**function_args)
125
- output_str = json.dumps(output)
126
- output_size = len(output_str.encode('utf-8'))
127
- print(f"\nOutput from function {tool.function.name}: {output_str[:64]}\n")
128
- return output_str, output_size
129
- except Exception as e:
130
- print(f"Error invoking function {tool.function.name}: {e}")
131
- return "No results found", 0
132
-
133
-
134
- def invoke_openai_function(function, function_args, function_name):
135
-
136
- json_body = function_args.get("json", None)
137
- path_params = function_args.get("path_params", None)
138
- fn_args = {"path_params": path_params, "data": json_body}
139
- headers = get_headers(function_name)
140
-
141
- query_params = function_args.get("params", {})
142
- params = get_params(function_name)
143
- query_params.update(params)
144
-
145
- output_fn = function(
146
- name=function_name,
147
- fn_args=fn_args,
148
- headers=headers,
149
- params=query_params,
150
- )
151
- print(f"\nOutput from function {function_name}: {output_fn.status_code} {output_fn.reason}\n")
152
- return {
153
- "status_code": output_fn.status_code,
154
- "text": output_fn.text,
155
- "reason": output_fn.reason,
156
- }
157
-
158
-
159
- def submit_tool_outputs(client, thread, run, tool_outputs):
160
- try:
161
- return client.beta.threads.runs.submit_tool_outputs_and_poll(
162
- thread_id=thread.id,
163
- run_id=run.id,
164
- tool_outputs=tool_outputs
165
- )
166
- except Exception as e:
167
- print("Failed to submit tool outputs:", e)
168
- return run
169
-
170
-
171
- def handle_run_completion(client, thread, run):
172
- if run.status == 'completed':
173
- messages = client.beta.threads.messages.list(thread_id=thread.id)
174
- return messages.data[0].content[0].text.value
175
- else:
176
- print("Failed to run assistant:", run.status)
177
- return run.status
178
-
179
- async def extract_and_structure_data(client, assistant, thread, prompt, user_provider_data, response_type, allowed_tools):
180
- formatted_prompt = prompt.format(input=user_provider_data)
181
- output = await run_assistant(client, assistant, thread, formatted_prompt, response_type, allowed_tools)
182
- return output
183
-
184
- # Function to get structured output from OpenAI API
185
- def get_structured_output(message: str, response_type):
186
- try:
187
- client = OpenAI()
188
- completion = client.beta.chat.completions.parse(
189
- model="gpt-4o-2024-08-06",
190
- messages=[
191
- {"role": "system", "content": "Extract structured content from input. Output is in JSON Format."},
192
- {"role": "user", "content": message},
193
- ],
194
- response_format=response_type,
195
- )
196
-
197
- response = completion.choices[0].message
198
- if response.parsed:
199
- return response.parsed, 'SUCCESS'
200
- elif response.refusal:
201
- logging.warning("ERROR: Refusal response: %s", response.refusal)
202
- return response.refusal, 'FAIL'
203
-
204
- except LengthFinishReasonError as e:
205
- logging.error(f"Too many tokens: {e}")
206
- raise HTTPException(status_code=502, detail="The request exceeded the maximum token limit.")
207
- except OpenAIError as e:
208
- logging.error(f"OpenAI API error: {e}")
209
- raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
210
- except Exception as e:
211
- logging.error(f"Unexpected error: {e}")
212
- raise HTTPException(status_code=500, detail="An unexpected error occurred while processing your request.")
213
-
214
- class RowItem(BaseModel):
215
- column_value: str
216
-
217
- class ResponseList(BaseModel):
218
- rows: List[RowItem]
219
-
220
- def lookup_response_type(name: str):
221
- for model in GLOBAL_DATA_MODELS:
222
- if model.__name__ == name:
223
- return model
224
- return None
225
-
226
- # Function to process a batch request
227
- async def process_agent_request(row_batch: List[Dict], steps: Dict, custom_instructions: str) -> List[Dict]:
228
- """
229
- Process agent request using the OpenAI client.
230
- """
231
- try:
232
- client = OpenAI()
233
- assistant = client.beta.assistants.create(
234
- name="AI Assistant",
235
- instructions=f"Hi, You are an AI Assistant. Help the user with their tasks.\n\n{custom_instructions}\n\n",
236
- tools=[],
237
- model="gpt-4o-2024-08-06"
238
- )
239
- thread = client.beta.threads.create()
240
-
241
- parsed_outputs = []
242
- for row in row_batch:
243
- try:
244
- input_data = json.dumps(row)
245
- output = {}
246
- for step in steps['steps']:
247
- type = step.get("response_type", None)
248
- if not type:
249
- type = "ResponseList"
250
- response_type = ResponseList
251
- else:
252
- response_type = lookup_response_type(step.get("response_type", None))
253
- if not response_type:
254
- response_type = ResponseList
255
- allowed_tools = step.get("allowed_tools", [])
256
- output = await extract_and_structure_data(client, assistant, thread, step["prompt"], input_data, response_type, allowed_tools)
257
- output_obj = json.loads(output)
258
- if 'ID' in row:
259
- output_obj['INPUT_ID'] = row['ID']
260
- input_data = output
261
- parsed_outputs.append(output_obj)
262
- except Exception as e:
263
- print(f"Error processing lead {row}: {e}")
264
- return parsed_outputs
265
- except Exception as e:
266
- print(f"An error occurred: {e}")
267
- return "Error Processing Leads"
268
- finally:
269
- try:
270
- client.beta.assistants.delete(assistant.id)
271
- except Exception as e:
272
- print(f"Error deleting assistant: {e}")
File without changes
File without changes