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.
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/PKG-INFO +1 -1
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/setup.py +1 -1
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_email.py +77 -31
- dhisana-0.0.1.dev33/src/dhisana/utils/generate_structured_output_internal.py +205 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openai_assistant_and_file_utils.py +1 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/PKG-INFO +1 -1
- dhisana-0.0.1.dev32/src/dhisana/utils/generate_structured_output_internal.py +0 -139
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/README.md +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/setup.cfg +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/agent_task.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/apollo_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_cadence.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_workflow.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/create_list_from_sales_navigator.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/create_smart_list.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/SOURCES.txt +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/requires.txt +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/tests/test_agent_tools.py +0 -0
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/create_list_from_sales_navigator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev32 → dhisana-0.0.1.dev33}/src/dhisana/utils/generate_linkedin_response_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|