dhisana 0.0.1.dev116__py3-none-any.whl → 0.0.1.dev236__py3-none-any.whl

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 (69) hide show
  1. dhisana/schemas/common.py +10 -1
  2. dhisana/schemas/sales.py +203 -22
  3. dhisana/utils/add_mapping.py +0 -2
  4. dhisana/utils/apollo_tools.py +739 -119
  5. dhisana/utils/built_with_api_tools.py +4 -2
  6. dhisana/utils/check_email_validity_tools.py +35 -18
  7. dhisana/utils/check_for_intent_signal.py +1 -2
  8. dhisana/utils/check_linkedin_url_validity.py +34 -8
  9. dhisana/utils/clay_tools.py +3 -2
  10. dhisana/utils/clean_properties.py +1 -4
  11. dhisana/utils/compose_salesnav_query.py +0 -1
  12. dhisana/utils/compose_search_query.py +7 -3
  13. dhisana/utils/composite_tools.py +0 -1
  14. dhisana/utils/dataframe_tools.py +2 -2
  15. dhisana/utils/email_body_utils.py +72 -0
  16. dhisana/utils/email_provider.py +174 -35
  17. dhisana/utils/enrich_lead_information.py +183 -53
  18. dhisana/utils/fetch_openai_config.py +129 -0
  19. dhisana/utils/field_validators.py +1 -1
  20. dhisana/utils/g2_tools.py +0 -1
  21. dhisana/utils/generate_content.py +0 -1
  22. dhisana/utils/generate_email.py +68 -23
  23. dhisana/utils/generate_email_response.py +294 -46
  24. dhisana/utils/generate_flow.py +0 -1
  25. dhisana/utils/generate_linkedin_connect_message.py +9 -2
  26. dhisana/utils/generate_linkedin_response_message.py +137 -66
  27. dhisana/utils/generate_structured_output_internal.py +317 -164
  28. dhisana/utils/google_custom_search.py +150 -44
  29. dhisana/utils/google_oauth_tools.py +721 -0
  30. dhisana/utils/google_workspace_tools.py +278 -54
  31. dhisana/utils/hubspot_clearbit.py +3 -1
  32. dhisana/utils/hubspot_crm_tools.py +718 -272
  33. dhisana/utils/instantly_tools.py +3 -1
  34. dhisana/utils/lusha_tools.py +10 -7
  35. dhisana/utils/mailgun_tools.py +150 -0
  36. dhisana/utils/microsoft365_tools.py +447 -0
  37. dhisana/utils/openai_assistant_and_file_utils.py +121 -177
  38. dhisana/utils/openai_helpers.py +8 -6
  39. dhisana/utils/parse_linkedin_messages_txt.py +1 -3
  40. dhisana/utils/profile.py +37 -0
  41. dhisana/utils/proxy_curl_tools.py +377 -76
  42. dhisana/utils/proxycurl_search_leads.py +426 -0
  43. dhisana/utils/research_lead.py +3 -3
  44. dhisana/utils/sales_navigator_crawler.py +1 -6
  45. dhisana/utils/salesforce_crm_tools.py +323 -50
  46. dhisana/utils/search_router.py +131 -0
  47. dhisana/utils/search_router_jobs.py +51 -0
  48. dhisana/utils/sendgrid_tools.py +126 -91
  49. dhisana/utils/serarch_router_local_business.py +75 -0
  50. dhisana/utils/serpapi_additional_tools.py +290 -0
  51. dhisana/utils/serpapi_google_jobs.py +117 -0
  52. dhisana/utils/serpapi_google_search.py +188 -0
  53. dhisana/utils/serpapi_local_business_search.py +129 -0
  54. dhisana/utils/serpapi_search_tools.py +360 -432
  55. dhisana/utils/serperdev_google_jobs.py +125 -0
  56. dhisana/utils/serperdev_local_business.py +154 -0
  57. dhisana/utils/serperdev_search.py +233 -0
  58. dhisana/utils/smtp_email_tools.py +178 -18
  59. dhisana/utils/test_connect.py +1603 -130
  60. dhisana/utils/trasform_json.py +3 -3
  61. dhisana/utils/web_download_parse_tools.py +0 -1
  62. dhisana/utils/zoominfo_tools.py +2 -3
  63. dhisana/workflow/test.py +1 -1
  64. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/METADATA +1 -1
  65. dhisana-0.0.1.dev236.dist-info/RECORD +100 -0
  66. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/WHEEL +1 -1
  67. dhisana-0.0.1.dev116.dist-info/RECORD +0 -83
  68. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/entry_points.txt +0 -0
  69. {dhisana-0.0.1.dev116.dist-info → dhisana-0.0.1.dev236.dist-info}/top_level.txt +0 -0
@@ -1,284 +1,443 @@
1
1
  import asyncio
2
2
  import hashlib
3
3
  import json
4
- import os
5
- import re
6
- import time
7
4
  import logging
8
- import uuid
9
- from typing import Any, Dict, List, Optional, Tuple
5
+ import random
10
6
 
11
7
  from fastapi import HTTPException
12
- from pydantic import BaseModel, TypeAdapter
8
+ from pydantic import BaseModel
13
9
 
14
- import openai
15
- from dhisana.utils import cache_output_tools
16
- from dhisana.utils.openai_helpers import get_openai_access_token
10
+ from openai import OpenAIError, RateLimitError
17
11
  from openai.lib._parsing._completions import type_to_response_format_param
12
+
18
13
  from json_repair import repair_json
19
14
 
15
+ from dhisana.utils import cache_output_tools
16
+ from dhisana.utils.fetch_openai_config import (
17
+ _extract_config,
18
+ create_async_openai_client,
19
+ )
20
+ from typing import Any, Dict, List, Optional, Tuple, Union
21
+
22
+ from openai import OpenAIError, RateLimitError
23
+ from pydantic import BaseModel
24
+
20
25
 
21
26
 
22
- # --------------------------------------------------------------------------
23
- # Utility: retrieve Vector Store and list its files (unchanged)
24
- # --------------------------------------------------------------------------
27
+ # ──────────────────────────────────────────────────────────────────────────────
28
+ # 2. Vector-store utilities (unchanged logic, new client factory)
29
+ # ──────────────────────────────────────────────────────────────────────────────
30
+
25
31
 
26
32
  async def get_vector_store_object(
27
- vector_store_id: str,
28
- tool_config: Optional[List[Dict]] = None
33
+ vector_store_id: str, tool_config: Optional[List[Dict]] = None
29
34
  ) -> Dict:
30
- openai_key = get_openai_access_token(tool_config)
31
- client_async = openai.AsyncOpenAI(api_key=openai_key)
32
- return await client_async.vector_stores.retrieve(vector_store_id=vector_store_id)
33
-
35
+ client_async = create_async_openai_client(tool_config)
36
+ try:
37
+ return await client_async.vector_stores.retrieve(vector_store_id=vector_store_id)
38
+ except OpenAIError as e:
39
+ logging.error(f"Error retrieving vector store {vector_store_id}: {e}")
40
+ return None
34
41
 
35
42
  async def list_vector_store_files(
36
- vector_store_id: str,
37
- tool_config: Optional[List[Dict]] = None
43
+ vector_store_id: str, tool_config: Optional[List[Dict]] = None
38
44
  ) -> List:
39
- openai_key = get_openai_access_token(tool_config)
40
- client_async = openai.AsyncOpenAI(api_key=openai_key)
45
+ client_async = create_async_openai_client(tool_config)
41
46
  page = await client_async.vector_stores.files.list(vector_store_id=vector_store_id)
42
47
  return page.data
43
48
 
44
49
 
50
+ # ──────────────────────────────────────────────────────────────────────────────
51
+ # 3. Core logic – only the client initialisation lines changed
52
+ # ──────────────────────────────────────────────────────────────────────────────
53
+
45
54
  async def get_structured_output_internal(
46
55
  prompt: str,
47
56
  response_format: BaseModel,
48
57
  effort: str = "low",
49
58
  use_web_search: bool = False,
50
- model: str = "gpt-4.1",
51
- tool_config: Optional[List[Dict]] = None
59
+ model: str = "gpt-5.1-chat",
60
+ tool_config: Optional[List[Dict]] = None,
61
+ use_cache: bool = True
52
62
  ):
53
63
  """
54
- Makes a direct call to the new Responses API for structured output,
55
- bypassing file_search. No vector store usage, no chain-of-thought.
56
-
57
- Updated behavior:
58
- - If there's a JSON parsing error on the initial parse, we try using
59
- jsonfix (repair_json) to repair minor errors, then re-parse.
60
- - If parsing still fails, return "FAIL".
61
- - If there's a refusal object from the model output,
62
- we log the refusal reason and return "FAIL".
63
- """
64
+ Makes a direct call to the new Responses API for structured output.
64
65
 
66
+ On a 429 (rate-limit) error the call is retried once after
67
+ 20 s + random exponential back-off.
68
+ """
65
69
  try:
66
- # For caching
70
+ # ─── caching bookkeeping ────────────────────────────────────────────
67
71
  response_type_str = response_format.__name__
68
- message_hash = hashlib.md5(prompt.encode('utf-8')).hexdigest()
69
- response_type_hash = hashlib.md5(response_type_str.encode('utf-8')).hexdigest()
72
+ message_hash = hashlib.md5(prompt.encode("utf-8")).hexdigest()
73
+ response_type_hash = hashlib.md5(response_type_str.encode("utf-8")).hexdigest()
70
74
  cache_key = f"{message_hash}:{response_type_hash}"
71
75
 
72
- # 1. Attempt to retrieve from cache
73
- cached_response = cache_output_tools.retrieve_output(
74
- "get_structured_output_internal",
75
- cache_key
76
- )
77
- if cached_response is not None:
78
- parsed_cached_response = response_format.parse_raw(cached_response)
79
- return parsed_cached_response, "SUCCESS"
76
+ if use_cache:
77
+ cached_response = cache_output_tools.retrieve_output(
78
+ "get_structured_output_internal", cache_key
79
+ )
80
+ if cached_response is not None:
81
+ parsed_cached_response = response_format.parse_raw(cached_response)
82
+ return parsed_cached_response, "SUCCESS"
80
83
 
81
- # 2. Build JSON schema format from the response_format
84
+ # ─── JSON schema for function calling ───────────────────────────────
82
85
  schema = type_to_response_format_param(response_format)
83
86
  json_schema_format = {
84
87
  "name": response_type_str,
85
88
  "type": "json_schema",
86
- "schema": schema['json_schema']['schema']
89
+ "schema": schema["json_schema"]["schema"],
87
90
  }
88
91
 
89
- # 3. Prepare OpenAI client
90
- openai_key = get_openai_access_token(tool_config)
91
- client_async = openai.AsyncOpenAI(api_key=openai_key)
92
+ # ─── client initialisation (NEW) ────────────────────────────────────
93
+ client_async = create_async_openai_client(tool_config)
92
94
 
93
- # 4. Decide if we need web_search or additional params
94
- if use_web_search and model.startswith("gpt-"):
95
- completion = await client_async.responses.create(
96
- input=[
97
- {"role": "system", "content": "You are a helpful AI. Output JSON only."},
98
- {"role": "user", "content": prompt}
99
- ],
100
- model=model,
101
- text={"format": json_schema_format},
102
- tool_choice="required",
103
- tools=[{"type": "web_search_preview"}],
104
- store=False,
105
- )
106
- else:
107
- # Only set reasoning if model starts with 'o'
108
- if model.startswith("o"):
109
- completion = await client_async.responses.create(
95
+ openai_cfg = _extract_config(tool_config, "openai")
96
+ # TODO: Azure OpenAI does not support web_search yet
97
+ if not openai_cfg:
98
+ use_web_search = False
99
+
100
+ # -------------------------------------------------------------------
101
+ # Internal helper to perform ONE attempt
102
+ # -------------------------------------------------------------------
103
+ async def _make_request():
104
+ if use_web_search and model.startswith("gpt-"):
105
+ return await client_async.responses.create(
110
106
  input=[
111
107
  {"role": "system", "content": "You are a helpful AI. Output JSON only."},
112
- {"role": "user", "content": prompt}
108
+ {"role": "user", "content": prompt},
113
109
  ],
114
110
  model=model,
115
- reasoning={"effort": effort},
116
111
  text={"format": json_schema_format},
112
+ tool_choice="required",
113
+ tools=[{"type": "web_search_preview"}],
117
114
  store=False,
118
115
  )
119
- else:
120
- completion = await client_async.responses.create(
116
+ if model.startswith("o"): # reasoning param only for "o" family
117
+ return await client_async.responses.create(
121
118
  input=[
122
119
  {"role": "system", "content": "You are a helpful AI. Output JSON only."},
123
- {"role": "user", "content": prompt}
120
+ {"role": "user", "content": prompt},
124
121
  ],
125
122
  model=model,
123
+ reasoning={"effort": effort},
126
124
  text={"format": json_schema_format},
127
125
  store=False,
128
126
  )
127
+ return await client_async.responses.create(
128
+ input=[
129
+ {"role": "system", "content": "You are a helpful AI. Output JSON only."},
130
+ {"role": "user", "content": prompt},
131
+ ],
132
+ model=model,
133
+ text={"format": json_schema_format},
134
+ store=False,
135
+ )
129
136
 
130
- # 5. Handle the model output
131
- if completion.output and len(completion.output) > 0:
132
- raw_text = None
137
+ # -------------------------------------------------------------------
138
+ # Call with one retry on 429
139
+ # -------------------------------------------------------------------
140
+ max_retries = 1
141
+ attempt = 0
142
+ while True:
143
+ try:
144
+ completion = await _make_request()
145
+ break # success → exit loop
146
+ except (RateLimitError, OpenAIError) as e:
147
+ # Detect 429 / rate-limit
148
+ is_rl = (
149
+ isinstance(e, RateLimitError)
150
+ or getattr(e, "status_code", None) == 429
151
+ or "rate_limit" in str(e).lower()
152
+ )
153
+ if is_rl and attempt < max_retries:
154
+ attempt += 1
155
+ # 20 s base + exponential jitter
156
+ wait_time = 20 + random.uniform(0, 2 ** attempt)
157
+ logging.warning(
158
+ f"Rate-limit hit (429). Waiting {wait_time:.2f}s then retrying "
159
+ f"({attempt}/{max_retries})."
160
+ )
161
+ await asyncio.sleep(wait_time)
162
+ continue # retry once
163
+ logging.error(f"OpenAI API error: {e}")
164
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
133
165
 
134
- # Iterate over outputs looking for text or refusal
166
+ # ─── handle model output (unchanged) ────────────────────────────────
167
+ if completion and completion.output and len(completion.output) > 0:
168
+ raw_text = None
135
169
  for out in completion.output:
136
- if out.type == 'message' and out.content:
170
+ if out.type == "message" and out.content:
137
171
  for content_item in out.content:
138
- # B) Otherwise, if it has a .text attribute, use it
139
- if hasattr(content_item, 'text'):
172
+ if hasattr(content_item, "text"):
140
173
  raw_text = content_item.text
141
174
  break
142
175
  else:
143
- logging.warning("request refused.", str(content_item))
176
+ logging.warning("request refused: %s", str(content_item))
144
177
  return "Request refused.", "FAIL"
145
178
  if raw_text:
146
179
  break
147
180
 
148
- # If no raw_text was found
149
181
  if not raw_text or not raw_text.strip():
150
182
  return "No text returned (possibly refusal or empty response)", "FAIL"
151
183
 
152
- # 6. Attempt to parse the returned JSON
153
184
  try:
154
185
  parsed_obj = response_format.parse_raw(raw_text)
155
- # Cache the successful result
156
186
  cache_output_tools.cache_output(
157
- "get_structured_output_internal",
158
- cache_key,
159
- parsed_obj.json()
187
+ "get_structured_output_internal", cache_key, parsed_obj.json()
160
188
  )
161
189
  return parsed_obj, "SUCCESS"
162
190
 
163
- except Exception as e:
164
- # If initial parse fails, attempt to fix JSON
191
+ except Exception:
165
192
  logging.warning("ERROR: Could not parse JSON from model output.")
166
- logging.warning("Attempting to fix JSON format using jsonfix...")
167
- logging.warning(raw_text)
168
-
169
193
  try:
170
- fixed_json = repair_json(raw_text) # This is your custom JSON fixer
194
+ fixed_json = repair_json(raw_text)
171
195
  parsed_obj = response_format.parse_raw(fixed_json)
172
-
173
- # Cache the successful result after fix
174
196
  cache_output_tools.cache_output(
175
- "get_structured_output_internal",
176
- cache_key,
177
- parsed_obj.json()
197
+ "get_structured_output_internal", cache_key, parsed_obj.json()
178
198
  )
179
199
  return parsed_obj, "SUCCESS"
180
-
181
200
  except Exception as e2:
182
- logging.warning(
183
- "ERROR: JSON parse still failed even after attempting to fix formatting."
184
- )
185
- logging.warning(str(e2))
201
+ logging.warning("JSON repair failed: %s", str(e2))
186
202
  return raw_text, "FAIL"
187
203
  else:
188
- # No output
189
204
  return "No output returned", "FAIL"
190
205
 
191
- except openai.OpenAIError as e:
206
+ except OpenAIError as e:
192
207
  logging.error(f"OpenAI API error: {e}")
193
- raise HTTPException(
194
- status_code=502,
195
- detail="Error communicating with the OpenAI API."
196
- )
208
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
197
209
  except Exception as e:
198
210
  logging.error(f"Unexpected error: {e}")
199
- raise HTTPException(
200
- status_code=500,
201
- detail="An unexpected error occurred while processing your request."
202
- )
211
+ raise HTTPException(status_code=500, detail="Unexpected server error.")
212
+
213
+
214
+
215
+ async def get_structured_output_with_mcp(
216
+ prompt: str,
217
+ response_format: BaseModel,
218
+ effort: str = "low",
219
+ use_web_search: bool = False,
220
+ model: str = "gpt-5.1-chat",
221
+ tool_config: Optional[List[Dict[str, Any]]] = None,
222
+ ) -> Tuple[Union[BaseModel, str], str]:
223
+ """
224
+ Sends a JSON-schema-constrained prompt to an OpenAI model, with an MCP
225
+ server configured as a `tool`.
226
+
227
+ * If the model returns a tool call that *requires approval*, the function
228
+ immediately returns a minimal object that satisfies `response_format`
229
+ with `"APPROVAL_PENDING"` in `response_summary`, along with the status
230
+ string ``"PENDING_APPROVAL"``.
231
+ * Once the tool has executed (the provider returns `mcp_tool_result`) or
232
+ the model replies directly with the JSON payload, the parsed object is
233
+ cached and returned with status ``"SUCCESS"``.
234
+ * Any MCP tool-listing messages are ignored.
235
+ """
236
+ # ─── Validate MCP configuration ────────────────────────────────────────────
237
+ mcp_cfg = _extract_config(tool_config, "mcpServer") or {}
238
+ server_label: str = mcp_cfg.get("serverLabel", "")
239
+ server_url: str | None = mcp_cfg.get("serverUrl")
240
+ api_key_header_name: str | None = mcp_cfg.get("apiKeyHeaderName")
241
+ api_key_header_value: str | None = mcp_cfg.get("apiKeyHeaderValue")
242
+
243
+ if not (server_url and api_key_header_name and api_key_header_value):
244
+ raise HTTPException(400, detail="MCP server configuration incomplete.")
245
+
246
+ # ─── Cache key (prompt + schema) ──────────────────────────────────────────
247
+ response_type_str = response_format.__name__
248
+ cache_key = (
249
+ f"{hashlib.md5(prompt.encode()).hexdigest()}:"
250
+ f"{hashlib.md5(response_type_str.encode()).hexdigest()}"
251
+ )
252
+ if (cached := cache_output_tools.retrieve_output("get_structured_output_with_mcp", cache_key)):
253
+ return response_format.parse_raw(cached), "SUCCESS"
254
+
255
+ # ─── JSON-schema format for `text` param ──────────────────────────────────
256
+ schema_cfg = type_to_response_format_param(response_format)
257
+ json_schema_format = {
258
+ "name": response_type_str,
259
+ "type": "json_schema",
260
+ "schema": schema_cfg["json_schema"]["schema"],
261
+ }
262
+
263
+ # ─── Build tool list ──────────────────────────────────────────────────────
264
+ tools: List[Dict[str, Any]] = [
265
+ {
266
+ "type": "mcp",
267
+ "server_label": server_label,
268
+ "server_url": server_url,
269
+ "headers": {api_key_header_name: api_key_header_value},
270
+ "require_approval": "never"
271
+ }
272
+ ]
273
+ if use_web_search and model.startswith("gpt-"):
274
+ tools.append({"type": "web_search_preview"})
203
275
 
276
+ # ─── Async OpenAI client ──────────────────────────────────────────────────
277
+ client_async = create_async_openai_client(tool_config)
204
278
 
205
- # --------------------------------------------------------------------------
206
- # get_structured_output_with_assistant_and_vector_store:
207
- # Similarly updated to provide search for first output .type == 'message'
208
- # --------------------------------------------------------------------------
279
+ async def _make_request():
280
+ kwargs: Dict[str, Any] = {
281
+ "input": [
282
+ {"role": "system", "content": "You are a helpful AI. Output JSON only."},
283
+ {"role": "user", "content": prompt},
284
+ ],
285
+ "model": model,
286
+ "text": {"format": json_schema_format},
287
+ "store": False,
288
+ "tools": tools,
289
+ "tool_choice": "required",
290
+ }
291
+ if model.startswith("o"):
292
+ kwargs["reasoning"] = {"effort": effort}
293
+ return await client_async.responses.create(**kwargs)
294
+
295
+ # ─── Retry once for 429s ──────────────────────────────────────────────────
296
+ for attempt in range(2):
297
+ try:
298
+ completion = await _make_request()
299
+ break
300
+ except (RateLimitError, OpenAIError) as exc:
301
+ if attempt == 0 and (
302
+ isinstance(exc, RateLimitError)
303
+ or getattr(exc, "status_code", None) == 429
304
+ or "rate_limit" in str(exc).lower()
305
+ ):
306
+ sleep_for = 20 + random.uniform(0, 2.0)
307
+ logging.warning("429 rate-limit hit; retrying in %.1fs", sleep_for)
308
+ await asyncio.sleep(sleep_for)
309
+ continue
310
+ logging.error("OpenAI API error: %s", exc)
311
+ raise HTTPException(502, detail="Error communicating with the OpenAI API.") from exc
312
+ else: # pragma: no cover
313
+ raise HTTPException(502, detail="OpenAI request retry loop failed.")
314
+
315
+ # ─── Parse the model’s structured output ──────────────────────────────────
316
+ if not (completion and completion.output):
317
+ return "No output returned", "FAIL"
318
+
319
+ raw_text: str | None = None
320
+ status: str = "SUCCESS"
321
+
322
+ for out in completion.output:
323
+ # 1️⃣ Human approval required
324
+ if out.type == "mcp_approval_request":
325
+ logging.info("Tool call '%s' awaiting approval", out.name)
326
+ placeholder_obj = response_format.parse_obj({"response_summary": "APPROVAL_PENDING"})
327
+ return placeholder_obj, "PENDING_APPROVAL"
328
+
329
+ # 2️⃣ Ignore capability listings
330
+ if out.type == "mcp_list_tools":
331
+ continue
332
+
333
+ # 3️⃣ Tool finished: provider returned result object
334
+ if out.type == "mcp_tool_result":
335
+ try:
336
+ # If result already matches schema, emit directly
337
+ raw_text = (
338
+ json.dumps(out.result)
339
+ if isinstance(out.result, (dict, list))
340
+ else json.dumps({"response_summary": str(out.result)})
341
+ )
342
+ except Exception: # pragma: no cover
343
+ raw_text = json.dumps({"response_summary": "TOOL_EXECUTION_COMPLETE"})
344
+ break
345
+
346
+ # 4️⃣ Regular assistant message
347
+ if out.type == "message" and out.content:
348
+ for c in out.content:
349
+ if hasattr(c, "text") and c.text:
350
+ raw_text = c.text
351
+ break
352
+ if raw_text:
353
+ break
354
+
355
+ # 5️⃣ Anything else
356
+ logging.debug("Unhandled output type: %s", out.type)
357
+
358
+ if not raw_text or not raw_text.strip():
359
+ return "No response", status
360
+
361
+ # ─── Convert JSON -> pydantic object, with repair fallback ────────────────
362
+ try:
363
+ parsed_obj = response_format.parse_raw(raw_text)
364
+ except Exception:
365
+ logging.warning("Initial parse failed; attempting JSON repair")
366
+ parsed_obj = response_format.parse_raw(repair_json(raw_text))
367
+
368
+ # ─── Cache & return ───────────────────────────────────────────────────────
369
+ cache_output_tools.cache_output(
370
+ "get_structured_output_with_mcp", cache_key, parsed_obj.json()
371
+ )
372
+ return parsed_obj, status
209
373
 
210
374
  async def get_structured_output_with_assistant_and_vector_store(
211
375
  prompt: str,
212
376
  response_format: BaseModel,
213
377
  vector_store_id: str,
214
378
  effort: str = "low",
215
- tool_config: Optional[List[Dict]] = None
379
+ model="gpt-5.1-chat",
380
+ tool_config: Optional[List[Dict]] = None,
381
+ use_cache: bool = True
216
382
  ):
217
383
  """
218
- If the vector store has NO files, call get_structured_output_internal directly.
219
- Otherwise, do a single call to the new Responses API with a 'file_search' tool
220
- to incorporate vector-store knowledge.
384
+ Same logic, now uses create_async_openai_client().
221
385
  """
222
386
  try:
223
- # 1. Ensure vector store exists
224
- await get_vector_store_object(vector_store_id, tool_config)
225
-
226
- # 2. Check if the vector store contains any files
387
+ vector_store = await get_vector_store_object(vector_store_id, tool_config)
388
+ if not vector_store:
389
+ return await get_structured_output_internal(
390
+ prompt, response_format, tool_config=tool_config
391
+ )
392
+
227
393
  files = await list_vector_store_files(vector_store_id, tool_config)
228
394
  if not files:
229
- # No files => just do the internal structured approach
230
- return await get_structured_output_internal(prompt, response_format, tool_config=tool_config)
395
+ return await get_structured_output_internal(
396
+ prompt, response_format, tool_config=tool_config
397
+ )
231
398
 
232
- # 3. If files exist => do a single "Responses" call with file_search
233
399
  response_type_str = response_format.__name__
234
- message_hash = hashlib.md5(prompt.encode('utf-8')).hexdigest()
235
- response_type_hash = hashlib.md5(response_type_str.encode('utf-8')).hexdigest()
400
+ message_hash = hashlib.md5(prompt.encode("utf-8")).hexdigest()
401
+ response_type_hash = hashlib.md5(response_type_str.encode("utf-8")).hexdigest()
236
402
  cache_key = f"{message_hash}:{response_type_hash}"
237
- cached_response = cache_output_tools.retrieve_output(
238
- "get_structured_output_with_assistant_and_vector_store",
239
- cache_key
240
- )
241
- if cached_response is not None:
242
- parsed_cached_response = response_format.parse_raw(cached_response)
243
- return parsed_cached_response, "SUCCESS"
244
403
 
404
+ if use_cache:
405
+ cached_response = cache_output_tools.retrieve_output(
406
+ "get_structured_output_with_assistant_and_vector_store", cache_key
407
+ )
408
+ if cached_response is not None:
409
+ parsed_cached_response = response_format.model_validate_json(cached_response)
410
+ return parsed_cached_response, "SUCCESS"
411
+
245
412
  schema = type_to_response_format_param(response_format)
246
413
  json_schema_format = {
247
414
  "name": response_type_str,
248
415
  "type": "json_schema",
249
- "schema": schema['json_schema']['schema']
416
+ "schema": schema["json_schema"]["schema"],
250
417
  }
251
418
 
252
- openai_key = get_openai_access_token(tool_config)
253
- client_async = openai.AsyncOpenAI(api_key=openai_key)
419
+ client_async = create_async_openai_client(tool_config)
254
420
 
255
- # Single call to the new Responses API
256
421
  completion = await client_async.responses.create(
257
422
  input=[
258
423
  {"role": "system", "content": "You are a helpful AI. Output JSON only."},
259
- {"role": "user", "content": prompt}
424
+ {"role": "user", "content": prompt},
260
425
  ],
261
- model="gpt-4.1",
426
+ model=model,
262
427
  text={"format": json_schema_format},
263
- tools=[{
264
- "type": "file_search",
265
- "vector_store_ids": [vector_store_id],
266
- }],
267
- reasoning={"effort": effort},
428
+ tools=[{"type": "file_search", "vector_store_ids": [vector_store_id]}],
268
429
  tool_choice="required",
269
- store=False
430
+ store=False,
270
431
  )
271
432
 
272
- if completion.output and len(completion.output) > 0:
433
+ if completion and completion.output and len(completion.output) > 0:
273
434
  raw_text = None
274
- # Find the first output whose type is 'message'
275
435
  for out in completion.output:
276
- if out.type == 'message' and out.content and len(out.content) > 0:
436
+ if out.type == "message" and out.content and len(out.content) > 0:
277
437
  raw_text = out.content[0].text
278
438
  break
279
439
 
280
440
  if not raw_text or not raw_text.strip():
281
- logging.error("No response text from the model.")
282
441
  raise HTTPException(status_code=502, detail="No response from the model.")
283
442
 
284
443
  try:
@@ -286,24 +445,18 @@ async def get_structured_output_with_assistant_and_vector_store(
286
445
  cache_output_tools.cache_output(
287
446
  "get_structured_output_with_assistant_and_vector_store",
288
447
  cache_key,
289
- parsed_obj.json()
448
+ parsed_obj.json(),
290
449
  )
291
450
  return parsed_obj, "SUCCESS"
292
- except Exception as e:
293
- logging.warning("ERROR: Model returned invalid JSON.")
451
+ except Exception:
452
+ logging.warning("Model returned invalid JSON.")
294
453
  return raw_text, "FAIL"
295
454
  else:
296
455
  return "No output returned", "FAIL"
297
456
 
298
- except openai.OpenAIError as e:
457
+ except OpenAIError as e:
299
458
  logging.error(f"OpenAI API error: {e}")
300
- raise HTTPException(
301
- status_code=502,
302
- detail="Error communicating with the OpenAI API."
303
- )
459
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
304
460
  except Exception as e:
305
461
  logging.error(f"Unexpected error: {e}")
306
- raise HTTPException(
307
- status_code=500,
308
- detail="An unexpected error occurred while processing your request."
309
- )
462
+ raise HTTPException(status_code=500, detail="Unexpected server error.")