dhisana 0.0.1.dev32__tar.gz → 0.0.1.dev33__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 (87) hide show
  1. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/setup.py +1 -1
  3. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_email.py +77 -31
  4. dhisana-0.0.1.dev33/src/dhisana/utils/generate_structured_output_internal.py +205 -0
  5. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openai_assistant_and_file_utils.py +1 -0
  6. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/PKG-INFO +1 -1
  7. dhisana-0.0.1.dev32/src/dhisana/utils/generate_structured_output_internal.py +0 -139
  8. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/README.md +0 -0
  9. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/pyproject.toml +0 -0
  10. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/setup.cfg +0 -0
  11. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/__init__.py +0 -0
  12. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/__init__.py +0 -0
  13. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/cli.py +0 -0
  14. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/datasets.py +0 -0
  15. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/models.py +0 -0
  16. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/predictions.py +0 -0
  17. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/schemas/__init__.py +0 -0
  18. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/schemas/common.py +0 -0
  19. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/schemas/sales.py +0 -0
  20. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/ui/__init__.py +0 -0
  21. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/ui/components.py +0 -0
  22. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/__init__.py +0 -0
  23. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/agent_task.py +0 -0
  24. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/agent_tools.py +0 -0
  25. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/apollo_tools.py +0 -0
  26. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  27. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/built_with_api_tools.py +0 -0
  28. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/cache_output_tools.py +0 -0
  29. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  30. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  31. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  32. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  33. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/clay_tools.py +0 -0
  34. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/company_utils.py +0 -0
  35. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_cadence.py +0 -0
  36. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  37. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_search_query.py +0 -0
  38. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  39. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_workflow.py +0 -0
  40. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/composite_tools.py +0 -0
  41. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/create_list_from_sales_navigator.py +0 -0
  42. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/create_smart_list.py +0 -0
  43. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/dataframe_tools.py +0 -0
  44. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/domain_parser.py +0 -0
  45. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/enrich_lead_information.py +0 -0
  46. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  47. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/g2_tools.py +0 -0
  48. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_content.py +0 -0
  49. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_email_response.py +0 -0
  50. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_flow.py +0 -0
  51. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  52. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  53. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/google_custom_search.py +0 -0
  54. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/google_workspace_tools.py +0 -0
  55. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  56. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  57. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/instantly_tools.py +0 -0
  58. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/linkedin_crawler.py +0 -0
  59. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/lusha_tools.py +0 -0
  60. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openai_helpers.py +0 -0
  61. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  62. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  63. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  64. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  65. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  66. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  67. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/python_function_to_tools.py +0 -0
  68. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/research_lead.py +0 -0
  69. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  70. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  71. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/sendgrid_tools.py +0 -0
  72. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  73. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/trasform_json.py +0 -0
  74. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  75. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/workflow_code_model.py +0 -0
  76. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/zoominfo_tools.py +0 -0
  77. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/__init__.py +0 -0
  78. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/agent.py +0 -0
  79. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/flow.py +0 -0
  80. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/task.py +0 -0
  81. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/test.py +0 -0
  82. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/SOURCES.txt +0 -0
  83. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/dependency_links.txt +0 -0
  84. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/entry_points.txt +0 -0
  85. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/requires.txt +0 -0
  86. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/top_level.txt +0 -0
  87. {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/tests/test_agent_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dhisana
3
- Version: 0.0.1.dev32
3
+ Version: 0.0.1.dev33
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-dev32',
5
+ version='0.0.1-dev33',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -27,36 +27,35 @@ async def generate_personalized_email_copy(
27
27
  email_context: ContentGenerationContext,
28
28
  variation: str,
29
29
  tool_config: Optional[List[Dict]] = None
30
- ):
30
+ ) -> dict:
31
31
  """
32
32
  Generate a personalized email copy using provided lead and campaign information with a template.
33
33
 
34
- This function sends an asynchronous request to generate an email copy based on the provided lead information and template.
35
-
36
- Parameters:
37
- emailcontext: Information about the lead, campaign.
38
- tool_config (Optional[List[Dict]]): Configuration for the tool (default is None).
39
-
40
- Returns:
41
- dict: The JSON response containing the email subject and body.
42
-
43
- Raises:
44
- Exception: If there is an error in processing the request.
34
+ Steps:
35
+ 1. Use either get_structured_output_with_assistant_and_vector_store or get_structured_output_internal
36
+ to get a first draft (subject & body).
37
+ 2. If the first draft was generated with 'get_structured_output_with_assistant_and_vector_store',
38
+ run that draft again through get_structured_output_internal with a refining prompt:
39
+
40
+ "Hi AI Assistant, I have the following draft ready for email,
41
+ following is more context for you, refine the email and give me a good output."
42
+
43
+ 3. Return the refined version (if step #2 happened) or the original draft (if #2 did not happen).
45
44
  """
46
45
  cleaned_context = cleanup_email_context(email_context)
47
- prompt = f"""
46
+
47
+ initial_prompt = f"""
48
48
  Hi AI Assistant,
49
49
 
50
50
  You’re an expert at crafting professional, concise, and compelling emails.
51
- Use the details below to ensuring personalization, a clear value proposition,
51
+ Use the details below to ensure personalization, a clear value proposition,
52
52
  and adherence to the specified email template. Avoid spam triggers or irrelevant info.
53
53
 
54
54
  **Important**:
55
55
  1. The final answer must be a JSON object containing only the fields 'subject' and 'body'.
56
56
  2. This is final copy of the email to be sent to the lead directly. DO NOT include any placeholders, comments or instructions in the final output.
57
- 3. If file_search is provided check if there are any relevant files to help provide more context for email.
57
+ 3. If file_search is provided, check if there are any relevant files to help provide more context for the email.
58
58
 
59
-
60
59
  The attached files have relevant information on case studies, product details, and customer testimonials.
61
60
 
62
61
  Steps:
@@ -96,25 +95,70 @@ async def generate_personalized_email_copy(
96
95
  such as city or school information. DO NOT USE any user identifiers, PII, tracking IDs, internal
97
96
  information like deal size, or any other sensitive information in email body generated.
98
97
  """
99
-
100
- if email_context.external_openai_vector_store_id:
101
- response, status = await get_structured_output_with_assistant_and_vector_store(prompt=prompt,
102
- response_format=EmailCopy,
103
- vector_store_id=email_context.external_openai_vector_store_id,
104
- tool_config=tool_config)
105
- else:
106
- response, status = await get_structured_output_internal(prompt, EmailCopy, tool_config=tool_config)
107
98
 
108
- if status != 'SUCCESS':
99
+ # 1. Generate initial draft
100
+ used_vector_store = False
101
+ if email_context.external_openai_vector_store_id:
102
+ # Generate the initial draft with get_structured_output_with_assistant_and_vector_store
103
+ initial_response, initial_status = await get_structured_output_with_assistant_and_vector_store(
104
+ prompt=initial_prompt,
105
+ response_format=EmailCopy,
106
+ vector_store_id=email_context.external_openai_vector_store_id,
107
+ tool_config=tool_config
108
+ )
109
+ used_vector_store = True
110
+ else:
111
+ # Otherwise, generate the initial draft with get_structured_output_internal
112
+ initial_response, initial_status = await get_structured_output_internal(
113
+ prompt=initial_prompt,
114
+ response_format=EmailCopy,
115
+ tool_config=tool_config
116
+ )
117
+
118
+ if initial_status != 'SUCCESS':
109
119
  raise Exception("Error in generating the personalized email.")
110
- return response.model_dump()
120
+
121
+ # 2. If we used the vector store, refine the draft once more using get_structured_output_internal
122
+ if used_vector_store:
123
+ # This is the "second time" prompt that refines the existing draft
124
+ refine_prompt = f"""
125
+ Hi AI Assistant, I have following draft ready for email:
126
+
127
+ Subject: {initial_response.subject}
128
+ Body: {initial_response.body}
129
+
130
+ Following is more context for you:
131
+ {cleaned_context.model_dump()}
132
+
133
+ Please refine the email and give me a good output.
134
+
135
+ Important:
136
+ 1. The final answer must be a JSON object with 'subject' and 'body' only.
137
+ 2. Keep it concise, professional, and relevant.
138
+ 3. Do not add any placeholders or instructions in the final output.
139
+ """
140
+
141
+ refined_response, refined_status = await get_structured_output_internal(
142
+ prompt=refine_prompt,
143
+ response_format=EmailCopy,
144
+ tool_config=tool_config
145
+ )
146
+
147
+ if refined_status != 'SUCCESS':
148
+ raise Exception("Error in refining the personalized email.")
149
+
150
+ # Return the refined email copy
151
+ return refined_response.model_dump()
152
+
153
+ # 3. Otherwise, just return the initial draft
154
+ return initial_response.model_dump()
111
155
 
112
156
  @assistant_tool
113
157
  async def generate_personalized_email(
114
158
  email_context: ContentGenerationContext,
115
159
  number_of_variations: int = 3,
116
160
  tool_config: Optional[List[Dict]] = None
117
- ):
161
+ ) -> List[dict]:
118
162
  """
119
163
  Generate a personalized email copy using provided lead and campaign information with a template.
120
164
 
@@ -129,22 +173,24 @@ async def generate_personalized_email(
129
173
  Raises:
130
174
  Exception: If there is an error in processing the request.
131
175
  """
176
+ # Just a few frameworks for demonstration
132
177
  variation_specs = [
133
178
  "Use PAS (Problem, Agitate, Solve) framework to write up email.",
134
- "Use VETO framework (Value, Evidence, Tie, Offer) to compose email.Explain how the product addresses the company’s current goals and requirements.",
179
+ "Use VETO framework (Value, Evidence, Tie, Offer) to compose email. Explain how the product addresses the company’s current goals and requirements.",
135
180
  "Use AIDA framework (Attention, Interest, Desire, Action) to compose email.",
136
181
  "Use SPIN (Situation, Problem, Implication, Need-Payoff) framework to write up email.",
137
182
  "Use BANT (Budget, Authority, Need, Timeline) framework to write up email.",
138
- "Use P-S-B (Pain, Solution, Benefit) framework to write up email."
139
- "Use The 3-Bullet Approach (1. Industry Trend or Pain, 2. Value Statement 3. Simple Ask) framework to write up email. Keep it under 100 words."
183
+ "Use P-S-B (Pain, Solution, Benefit) framework to write up email. Use The 3-Bullet Approach. Keep it under 100 words.",
140
184
  "Use Hook, Insight, Offer framework to write up email."
141
185
  ]
186
+
142
187
  email_variations = []
143
188
  for i in range(number_of_variations):
144
189
  try:
190
+ variation_text = variation_specs[i % len(variation_specs)]
145
191
  email_copy = await generate_personalized_email_copy(
146
192
  email_context,
147
- variation_specs[i % 3],
193
+ variation_text,
148
194
  tool_config
149
195
  )
150
196
  email_variations.append(email_copy)
@@ -0,0 +1,205 @@
1
+ import asyncio
2
+ import hashlib
3
+ import json
4
+ import os
5
+ import re
6
+ import time
7
+ import logging
8
+ import uuid
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from fastapi import HTTPException
12
+ from openai import AsyncOpenAI, OpenAI, OpenAIError, LengthFinishReasonError
13
+ from pydantic import BaseModel, TypeAdapter
14
+
15
+ from dhisana.utils import cache_output_tools
16
+ from dhisana.utils.openai_assistant_and_file_utils import (
17
+ add_user_message,
18
+ create_and_retrieve_run,
19
+ create_assistant,
20
+ create_thread,
21
+ delete_assistant,
22
+ get_first_message_content,
23
+ get_run_status,
24
+ )
25
+ from dhisana.utils.openai_helpers import get_openai_access_token
26
+
27
+ # Instantiate the synchronous OpenAI client (for vector store calls).
28
+ client = OpenAI()
29
+
30
+ async def get_vector_store_object(vector_store_id: str, tool_config: Optional[List[Dict]] = None) -> Dict:
31
+ """
32
+ Retrieve the vector store object (dict) via the SDK.
33
+ """
34
+ return await asyncio.to_thread(
35
+ lambda: client.beta.vector_stores.retrieve(vector_store_id=vector_store_id)
36
+ )
37
+
38
+ async def list_vector_store_files(
39
+ vector_store_id: str,
40
+ tool_config: Optional[List[Dict]] = None
41
+ ) -> List:
42
+ """
43
+ Retrieve the list of files (VectorStoreFile objects) for a given vector store.
44
+ """
45
+ page = await asyncio.to_thread(
46
+ lambda: client.beta.vector_stores.files.list(vector_store_id=vector_store_id)
47
+ )
48
+ return page.data # 'data' is the list of VectorStoreFile objects
49
+
50
+ async def get_structured_output_internal(
51
+ prompt: str,
52
+ response_format,
53
+ tool_config: Optional[List[Dict]] = None
54
+ ):
55
+ """
56
+ Makes a direct call to the internal structured output approach,
57
+ bypassing vector store or other chain-of-thought tools.
58
+ """
59
+ try:
60
+ response_type_str = response_format.__name__
61
+ message_hash = hashlib.md5(prompt.encode('utf-8')).hexdigest()
62
+ response_type_hash = hashlib.md5(response_type_str.encode('utf-8')).hexdigest()
63
+ cache_key = f"{message_hash}:{response_type_hash}"
64
+ cached_response = cache_output_tools.retrieve_output("get_structured_output_internal", cache_key)
65
+ if cached_response is not None:
66
+ parsed_cached_response = response_format.parse_raw(cached_response)
67
+ return parsed_cached_response, 'SUCCESS'
68
+
69
+ OPENAI_KEY = get_openai_access_token(tool_config)
70
+ client_async = AsyncOpenAI(api_key=OPENAI_KEY)
71
+ completion = await client_async.beta.chat.completions.parse(
72
+ model="o3-mini",
73
+ messages=[
74
+ {"role": "system", "content": "Extract structured content from input. Output is in JSON Format."},
75
+ {"role": "user", "content": prompt},
76
+ ],
77
+ response_format=response_format
78
+ )
79
+ response = completion.choices[0].message
80
+ if response.parsed:
81
+ cache_output_tools.cache_output(
82
+ "get_structured_output_internal",
83
+ cache_key,
84
+ response.parsed.json()
85
+ )
86
+ return response.parsed, 'SUCCESS'
87
+ elif response.refusal:
88
+ logging.warning("ERROR: Refusal response: %s", response.refusal)
89
+ return response.refusal, 'FAIL'
90
+
91
+ except LengthFinishReasonError as e:
92
+ logging.error(f"Too many tokens: {e}")
93
+ raise HTTPException(status_code=502, detail="The request exceeded the maximum token limit.")
94
+ except OpenAIError as e:
95
+ logging.error(f"OpenAI API error: {e}")
96
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
97
+ except Exception as e:
98
+ logging.error(f"Unexpected error: {e}")
99
+ raise HTTPException(
100
+ status_code=500,
101
+ detail="An unexpected error occurred while processing your request."
102
+ )
103
+
104
+ async def get_structured_output_with_assistant_and_vector_store(
105
+ prompt: str,
106
+ response_format,
107
+ vector_store_id: str,
108
+ tool_config: Optional[List[Dict]] = None
109
+ ):
110
+ """
111
+ If the vector store has NO files, call get_structured_output_internal directly.
112
+ Otherwise, proceed with the assistant flow.
113
+ """
114
+ assistant = None
115
+ try:
116
+ # 1. Retrieve the vector store object (to verify it exists or get usage).
117
+ _ = await get_vector_store_object(vector_store_id, tool_config)
118
+
119
+ # 2. Check if the vector store contains any files.
120
+ files = await list_vector_store_files(vector_store_id, tool_config)
121
+ if not files:
122
+ # If no files, just call our internal structured output function.
123
+ return await get_structured_output_internal(prompt, response_format, tool_config)
124
+
125
+ # 3. If there are files, proceed with the assistant-based approach.
126
+ response_type_str = response_format.__name__
127
+ message_hash = hashlib.md5(prompt.encode('utf-8')).hexdigest()
128
+ response_type_hash = hashlib.md5(response_type_str.encode('utf-8')).hexdigest()
129
+ cache_key = f"{message_hash}:{response_type_hash}"
130
+ cached_response = cache_output_tools.retrieve_output(
131
+ "get_structured_output_with_assistant_and_vector_store",
132
+ cache_key
133
+ )
134
+ if cached_response is not None:
135
+ parsed_cached_response = response_format.parse_raw(cached_response)
136
+ return parsed_cached_response, 'SUCCESS'
137
+
138
+ assistant_name = f"assistant_{uuid.uuid4().hex}"
139
+ instructions = "Hi, You are a helpful AI Assistant. Help the users with the given instructions."
140
+ tools = []
141
+ assistant, vector_store_id = await create_assistant(
142
+ assistant_name,
143
+ instructions,
144
+ tools,
145
+ vector_store_id,
146
+ tool_config
147
+ )
148
+
149
+ metadata = {"assistant_id": assistant.id, "assistant_name": assistant_name}
150
+ thread = await create_thread(metadata, vector_store_id=vector_store_id, tool_config=tool_config)
151
+ await add_user_message(prompt, thread, tool_config)
152
+
153
+ run = await create_and_retrieve_run(
154
+ thread.id,
155
+ assistant.id,
156
+ instructions,
157
+ tools,
158
+ response_format,
159
+ tool_config
160
+ )
161
+
162
+ MAX_WAIT_TIME = 180 # 3 minutes
163
+ start_time = time.time()
164
+ while run.status not in ["completed", "failed"]:
165
+ if time.time() - start_time > MAX_WAIT_TIME:
166
+ logging.error("Run did not complete within the maximum wait time of 3 minutes.")
167
+ break
168
+ await asyncio.sleep(2)
169
+ run = await get_run_status(thread.id, run.id, tool_config)
170
+
171
+ if run.status == 'completed':
172
+ response_text = await get_first_message_content(thread.id, tool_config)
173
+ pattern = r'【\d+:\d+†[^】]+】'
174
+ response_text = re.sub(pattern, '', response_text)
175
+ if response_text:
176
+ response = TypeAdapter(response_format).validate_json(response_text)
177
+ cache_output_tools.cache_output(
178
+ "get_structured_output_with_assistant_and_vector_store",
179
+ cache_key,
180
+ json.dumps(response.model_dump())
181
+ )
182
+ else:
183
+ raise HTTPException(status_code=502, detail="No response from the assistant.")
184
+ return response, 'SUCCESS'
185
+ else:
186
+ raise HTTPException(
187
+ status_code=502,
188
+ detail=f"Run failed with status: {run.status}"
189
+ )
190
+
191
+ except LengthFinishReasonError as e:
192
+ logging.error(f"Too many tokens: {e}")
193
+ raise HTTPException(status_code=502, detail="The request exceeded the maximum token limit.")
194
+ except OpenAIError as e:
195
+ logging.error(f"OpenAI API error: {e}")
196
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
197
+ except Exception as e:
198
+ logging.error(f"Unexpected error: {e}")
199
+ raise HTTPException(
200
+ status_code=500,
201
+ detail="An unexpected error occurred while processing your request."
202
+ )
203
+ finally:
204
+ if assistant:
205
+ await delete_assistant(assistant.id, tool_config)
@@ -242,6 +242,7 @@ async def create_and_retrieve_run(
242
242
  instructions=prompt,
243
243
  response_format=response_format_schema,
244
244
  tools=all_tools,
245
+ tool_choice={"type": "file_search"}
245
246
  )
246
247
  return await client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)
247
248
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: dhisana
3
- Version: 0.0.1.dev32
3
+ Version: 0.0.1.dev33
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,139 +0,0 @@
1
-
2
- import asyncio
3
- import hashlib
4
- import json
5
- import re
6
- import time
7
- import logging
8
- from typing import Any, Dict, List, Optional, Tuple
9
- import uuid
10
-
11
- from fastapi import HTTPException
12
- from openai import AsyncOpenAI, OpenAIError, LengthFinishReasonError
13
- from pydantic import BaseModel, TypeAdapter
14
-
15
- from dhisana.utils import cache_output_tools
16
- from dhisana.utils.openai_assistant_and_file_utils import add_user_message, create_and_retrieve_run, create_assistant, create_thread, delete_assistant, get_first_message_content, get_run_status
17
- from dhisana.utils.openai_helpers import get_openai_access_token
18
-
19
- # Get structutred output based on input message using OpenAI API
20
- async def get_structured_output_internal(message: str, response_type, tool_config: Optional[List[Dict]] = None):
21
- try:
22
- # Use the class name instead of serializing the class
23
- response_type_str = response_type.__name__
24
-
25
- # Create unique hashes for message and response_type
26
- message_hash = hashlib.md5(message.encode('utf-8')).hexdigest()
27
- response_type_hash = hashlib.md5(response_type_str.encode('utf-8')).hexdigest()
28
-
29
- # Generate the cache key
30
- cache_key = f"{message_hash}:{response_type_hash}"
31
- cached_response = cache_output_tools.retrieve_output(f"get_structured_output_internal", cache_key)
32
- if cached_response is not None:
33
- parsed_cached_response = response_type.parse_raw(cached_response)
34
- return parsed_cached_response, 'SUCCESS'
35
-
36
- OPENAI_KEY = get_openai_access_token(tool_config)
37
- client = AsyncOpenAI(api_key=OPENAI_KEY)
38
- completion = await client.beta.chat.completions.parse(
39
- model="o3-mini",
40
- messages=[
41
- {"role": "system", "content": "Extract structured content from input. Output is in JSON Format."},
42
- {"role": "user", "content": message},
43
- ],
44
- response_format=response_type
45
- )
46
-
47
- response = completion.choices[0].message
48
- if response.parsed:
49
- cache_output_tools.cache_output("get_structured_output_internal", cache_key, response.parsed.json())
50
- return response.parsed, 'SUCCESS'
51
- elif response.refusal:
52
- logging.warning("ERROR: Refusal response: %s", response.refusal)
53
- return response.refusal, 'FAIL'
54
-
55
- except LengthFinishReasonError as e:
56
- logging.error(f"Too many tokens: {e}")
57
- raise HTTPException(status_code=502, detail="The request exceeded the maximum token limit.")
58
- except OpenAIError as e:
59
- logging.error(f"OpenAI API error: {e}")
60
- raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
61
- except Exception as e:
62
- logging.error(f"Unexpected error: {e}")
63
- raise HTTPException(status_code=500, detail="An unexpected error occurred while processing your request.")
64
-
65
-
66
- async def get_structured_output_with_assistant_and_vector_store(prompt: str, response_format, vector_store_id: str, tool_config: Optional[List[Dict]] = None):
67
- assistant = None
68
- try:
69
- # Use the class name instead of serializing the class
70
- response_type_str = response_format.__name__
71
-
72
- # Create unique hashes for message and response_type
73
- message_hash = hashlib.md5(prompt.encode('utf-8')).hexdigest()
74
- response_type_hash = hashlib.md5(response_type_str.encode('utf-8')).hexdigest()
75
-
76
- # Generate the cache key
77
- cache_key = f"{message_hash}:{response_type_hash}"
78
- cached_response = cache_output_tools.retrieve_output(f"get_structured_output_with_assistant_and_vector_store", cache_key)
79
- if cached_response is not None:
80
- parsed_cached_response = response_format.parse_raw(cached_response)
81
- return parsed_cached_response, 'SUCCESS'
82
-
83
- # Step 1: Create an assistant with the specified vector store attached
84
- assistant_name = "assistant_" + uuid.uuid4().hex
85
- instructions = "Hi, You are a helpful AI Assistant. Help the users with the given instructions."
86
- tools = []
87
- assistant, vector_store_id = await create_assistant(assistant_name, instructions, tools, vector_store_id, tool_config)
88
-
89
- # Step 2: Create a new thread with the user's prompt
90
- metadata = {"assistant_id": assistant.id, "assistant_name": assistant_name}
91
- thread = await create_thread(metadata, vector_store_id=vector_store_id, tool_config=tool_config)
92
-
93
- message = await add_user_message(prompt, thread, tool_config)
94
- tools = []
95
-
96
- # Step 3: Initiate a run with the specified response format
97
- run = await create_and_retrieve_run(thread.id, assistant.id, instructions, tools, response_format, tool_config)
98
-
99
- # Step 4: Poll the run status until completion
100
- MAX_WAIT_TIME = 180 # 3 minutes in seconds
101
- start_time = time.time()
102
- while run.status not in ["completed", "failed"]:
103
- if time.time() - start_time > MAX_WAIT_TIME:
104
- logging.error("Run did not complete within the maximum wait time of 3 minutes.")
105
- break
106
- await asyncio.sleep(2)
107
- run = await get_run_status(thread.id, run.id, tool_config)
108
-
109
- # Step 5: Check if the run completed successfully
110
- if run.status == 'completed':
111
- # Retrieve the assistant's response
112
- response_text = await get_first_message_content(thread.id, tool_config)
113
- pattern = r'【\d+:\d+†[^】]+】'
114
- response_text = re.sub(pattern, '', response_text)
115
- if response_text:
116
- response = TypeAdapter(response_format).validate_json(response_text)
117
- cache_output_tools.cache_output(
118
- "get_structured_output_with_assistant_and_vector_store",
119
- cache_key,
120
- json.dumps(response.model_dump())
121
- )
122
- else:
123
- raise HTTPException(status_code=502, detail="No response from the assistant.")
124
- return response, 'SUCCESS'
125
- else:
126
- raise HTTPException(status_code=502, detail=f"Run failed with status: {run.status}")
127
-
128
- except LengthFinishReasonError as e:
129
- logging.error(f"Too many tokens: {e}")
130
- raise HTTPException(status_code=502, detail="The request exceeded the maximum token limit.")
131
- except OpenAIError as e:
132
- logging.error(f"OpenAI API error: {e}")
133
- raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
134
- except Exception as e:
135
- logging.error(f"Unexpected error: {e}")
136
- raise HTTPException(status_code=500, detail="An unexpected error occurred while processing your request.")
137
- finally:
138
- if assistant:
139
- await delete_assistant(assistant.id, tool_config)
File without changes
File without changes