dhisana 0.0.1.dev265__tar.gz → 0.0.1.dev267__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 (121) hide show
  1. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/PKG-INFO +1 -1
  2. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/setup.py +1 -1
  3. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/email_provider.py +3 -0
  4. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_structured_output_internal.py +306 -59
  5. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/mailgun_tools.py +10 -7
  6. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/sendgrid_tools.py +12 -8
  7. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana.egg-info/PKG-INFO +1 -1
  8. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/README.md +0 -0
  9. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/pyproject.toml +0 -0
  10. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/setup.cfg +0 -0
  11. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/__init__.py +0 -0
  12. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/cli/__init__.py +0 -0
  13. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/cli/cli.py +0 -0
  14. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/cli/datasets.py +0 -0
  15. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/cli/models.py +0 -0
  16. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/cli/predictions.py +0 -0
  17. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/schemas/__init__.py +0 -0
  18. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/schemas/common.py +0 -0
  19. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/schemas/sales.py +0 -0
  20. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/ui/__init__.py +0 -0
  21. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/ui/components.py +0 -0
  22. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/__init__.py +0 -0
  23. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/add_mapping.py +0 -0
  24. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/agent_tools.py +0 -0
  25. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/apollo_tools.py +0 -0
  26. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  27. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/built_with_api_tools.py +0 -0
  28. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/cache_output_tools.py +0 -0
  29. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  30. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  31. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  32. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  33. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/clay_tools.py +0 -0
  34. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/clean_properties.py +0 -0
  35. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/company_utils.py +0 -0
  36. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  37. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/compose_search_query.py +0 -0
  38. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  39. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/composite_tools.py +0 -0
  40. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/dataframe_tools.py +0 -0
  41. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/domain_parser.py +0 -0
  42. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/email_body_utils.py +0 -0
  43. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/email_parse_helpers.py +0 -0
  44. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/enrich_lead_information.py +0 -0
  45. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  46. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/fetch_openai_config.py +0 -0
  47. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/field_validators.py +0 -0
  48. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/g2_tools.py +0 -0
  49. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_content.py +0 -0
  50. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_custom_message.py +0 -0
  51. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_email.py +0 -0
  52. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_email_response.py +0 -0
  53. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_flow.py +0 -0
  54. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  55. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  56. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  57. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/google_custom_search.py +0 -0
  58. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/google_oauth_tools.py +0 -0
  59. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/google_workspace_tools.py +0 -0
  60. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  61. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  62. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/instantly_tools.py +0 -0
  63. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/linkedin_crawler.py +0 -0
  64. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/lusha_tools.py +0 -0
  65. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/mailreach_tools.py +0 -0
  66. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/microsoft365_tools.py +0 -0
  67. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  68. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openai_helpers.py +0 -0
  69. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  70. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  71. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  72. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  73. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  74. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  75. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/profile.py +0 -0
  76. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  77. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  78. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/python_function_to_tools.py +0 -0
  79. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/research_lead.py +0 -0
  80. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  81. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  82. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/search_router.py +0 -0
  83. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/search_router_jobs.py +0 -0
  84. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  85. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  86. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  87. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serpapi_google_search.py +0 -0
  88. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  89. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  90. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  91. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serperdev_local_business.py +0 -0
  92. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/serperdev_search.py +0 -0
  93. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/smtp_email_tools.py +0 -0
  94. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/test_connect.py +0 -0
  95. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/trasform_json.py +0 -0
  96. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  97. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/workflow_code_model.py +0 -0
  98. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/utils/zoominfo_tools.py +0 -0
  99. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/workflow/__init__.py +0 -0
  100. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/workflow/agent.py +0 -0
  101. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/workflow/flow.py +0 -0
  102. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/workflow/task.py +0 -0
  103. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana/workflow/test.py +0 -0
  104. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana.egg-info/SOURCES.txt +0 -0
  105. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana.egg-info/dependency_links.txt +0 -0
  106. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana.egg-info/entry_points.txt +0 -0
  107. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana.egg-info/requires.txt +0 -0
  108. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/src/dhisana.egg-info/top_level.txt +0 -0
  109. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_agent_tools.py +0 -0
  110. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_apollo_company_search.py +0 -0
  111. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_apollo_lead_search.py +0 -0
  112. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_connectivity.py +0 -0
  113. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_email_body_utils.py +0 -0
  114. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_google_document.py +0 -0
  115. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_hubspot_call_logs.py +0 -0
  116. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_linkedin_serper.py +0 -0
  117. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_mailreach.py +0 -0
  118. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_mcp_connectivity.py +0 -0
  119. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_proxycurl_get_company_search_id.py +0 -0
  120. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_proxycurl_job_count.py +0 -0
  121. {dhisana-0.0.1.dev265 → dhisana-0.0.1.dev267}/tests/test_structured_output_with_mcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev265
3
+ Version: 0.0.1.dev267
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-dev265',
5
+ version='0.0.1-dev267',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -124,9 +124,12 @@ async def send_email_async(
124
124
 
125
125
  Returns whatever the underlying provider helper returns:
126
126
 
127
+ * Mailgun → str (message-id from Mailgun)
128
+ * SendGrid → str (X-Message-Id from SendGrid)
127
129
  * SMTP → str (Message-ID)
128
130
  * Microsoft 365 → str (message-id)
129
131
  * Google Workspace → str (message-id)
132
+ * Google OAuth → str (message-id)
130
133
  """
131
134
  # ------------------------------------------------------------------ #
132
135
  # 1) Try the preferred providers in order
@@ -19,6 +19,126 @@ from dhisana.utils.fetch_openai_config import (
19
19
  create_async_openai_client,
20
20
  )
21
21
 
22
+ # Import search and scrape utilities for web search tools
23
+ try:
24
+ from dhisana.utils.search_router import search_google_with_tools
25
+ except Exception:
26
+ async def search_google_with_tools(*a, **k):
27
+ return []
28
+
29
+ try:
30
+ from dhisana.utils.web_download_parse_tools import get_text_content_from_url
31
+ except Exception:
32
+ async def get_text_content_from_url(url: str) -> str:
33
+ return ""
34
+
35
+
36
+ # ──────────────────────────────────────────────────────────────────────────────
37
+ # Web search tool definitions for the Responses API
38
+ # ──────────────────────────────────────────────────────────────────────────────
39
+
40
+ SEARCH_GOOGLE_TOOL = {
41
+ "type": "function",
42
+ "name": "search_google",
43
+ "description": "Search Google for information. Returns a list of search results with titles, links, and snippets.",
44
+ "parameters": {
45
+ "type": "object",
46
+ "properties": {
47
+ "query": {
48
+ "type": "string",
49
+ "description": "The search query to look up on Google"
50
+ },
51
+ "num_results": {
52
+ "type": "integer",
53
+ "description": "Number of results to return (default: 5, max: 10)"
54
+ }
55
+ },
56
+ "required": ["query"],
57
+ "additionalProperties": False
58
+ }
59
+ }
60
+
61
+ FETCH_URL_CONTENT_TOOL = {
62
+ "type": "function",
63
+ "name": "fetch_url_content",
64
+ "description": "Fetch and extract text content from a URL. Use this to read the full content of a webpage.",
65
+ "parameters": {
66
+ "type": "object",
67
+ "properties": {
68
+ "url": {
69
+ "type": "string",
70
+ "description": "The URL to fetch content from"
71
+ }
72
+ },
73
+ "required": ["url"],
74
+ "additionalProperties": False
75
+ }
76
+ }
77
+
78
+
79
+ async def _execute_search_google(
80
+ query: str, num_results: int, tool_config: Optional[List[Dict]]
81
+ ) -> str:
82
+ """Execute Google search and return results as JSON string."""
83
+ try:
84
+ num_results = min(max(num_results, 1), 10)
85
+ raw = await search_google_with_tools(
86
+ query, number_of_results=num_results, offset=0, tool_config=tool_config
87
+ )
88
+ results = []
89
+ if isinstance(raw, list):
90
+ for item in raw:
91
+ try:
92
+ data = json.loads(item) if isinstance(item, str) else item
93
+ results.append({
94
+ "title": data.get("title", ""),
95
+ "link": data.get("link", ""),
96
+ "snippet": data.get("snippet", "")
97
+ })
98
+ except Exception:
99
+ continue
100
+ return json.dumps(results, default=str)
101
+ except Exception as e:
102
+ logging.warning("search_google tool failed: %s", e)
103
+ return json.dumps({"error": str(e)})
104
+
105
+
106
+ async def _execute_fetch_url_content(url: str) -> str:
107
+ """Fetch URL content and return as string."""
108
+ try:
109
+ content = await get_text_content_from_url(url)
110
+ if content:
111
+ max_len = 15000
112
+ if len(content) > max_len:
113
+ content = content[:max_len] + "\n... [content truncated]"
114
+ return content
115
+ return "Failed to fetch content from URL"
116
+ except Exception as e:
117
+ logging.warning("fetch_url_content tool failed for %s: %s", url, e)
118
+ return f"Error fetching URL: {str(e)}"
119
+
120
+
121
+ async def _execute_web_search_tool(
122
+ tool_name: str, args: dict, tool_config: Optional[List[Dict]]
123
+ ) -> str:
124
+ """Execute a web search tool and return the result as a string."""
125
+ if tool_name == "search_google":
126
+ query = args.get("query", "")
127
+ num_results = args.get("num_results", 5)
128
+ if not query:
129
+ return json.dumps({"error": "Missing required parameter: query"})
130
+ return await _execute_search_google(query, num_results, tool_config)
131
+
132
+ elif tool_name == "fetch_url_content":
133
+ url = args.get("url", "")
134
+ if not url:
135
+ return json.dumps({"error": "Missing required parameter: url"})
136
+ return await _execute_fetch_url_content(url)
137
+
138
+ else:
139
+ logging.warning(f"Unknown tool requested: {tool_name}")
140
+ return json.dumps({"error": f"Unknown tool: {tool_name}"})
141
+
22
142
 
23
143
  # ──────────────────────────────────────────────────────────────────────────────
24
144
  # 1. Helper functions
@@ -71,6 +191,9 @@ async def get_structured_output_internal(
71
191
 
72
192
  On a 429 (rate-limit) error the call is retried once after
73
193
  20 s + random exponential back-off.
194
+
195
+ If use_web_search=True, uses Google search and URL scraping tools
196
+ to enable web research (works with both OpenAI and Azure OpenAI).
74
197
  """
75
198
  try:
76
199
  # ─── caching bookkeeping ────────────────────────────────────────────
@@ -95,30 +218,24 @@ async def get_structured_output_internal(
95
218
  "schema": schema["json_schema"]["schema"],
96
219
  }
97
220
 
98
- # ─── client initialisation (NEW) ────────────────────────────────────
221
+ # ─── client initialisation ──────────────────────────────────────────
99
222
  client_async = create_async_openai_client(tool_config)
100
223
 
101
- openai_cfg = _extract_config(tool_config, "openai")
102
- # TODO: Azure OpenAI does not support web_search yet
103
- if not openai_cfg:
104
- use_web_search = False
224
+ # ─── Web search path (uses Google search + URL scraping tools) ──────
225
+ if use_web_search:
226
+ return await _get_structured_output_with_web_search(
227
+ client_async=client_async,
228
+ prompt=prompt,
229
+ response_format=response_format,
230
+ json_schema_format=json_schema_format,
231
+ model=model,
232
+ effort=effort,
233
+ tool_config=tool_config,
234
+ cache_key=cache_key,
235
+ )
105
236
 
106
- # -------------------------------------------------------------------
107
- # Internal helper to perform ONE attempt
108
- # -------------------------------------------------------------------
237
+ # ─── Standard path (no web search) ──────────────────────────────────
109
238
  async def _make_request():
110
- if use_web_search and model.startswith("gpt-"):
111
- return await client_async.responses.create(
112
- input=[
113
- {"role": "system", "content": "You are a helpful AI. Output JSON only."},
114
- {"role": "user", "content": prompt},
115
- ],
116
- model=model,
117
- text={"format": json_schema_format},
118
- tool_choice="required",
119
- tools=[{"type": "web_search_preview"}],
120
- store=False,
121
- )
122
239
  if model.startswith("o"): # reasoning param only for "o" family
123
240
  return await client_async.responses.create(
124
241
  input=[
@@ -175,45 +292,8 @@ async def get_structured_output_internal(
175
292
  logging.error(f"OpenAI API error: {e}")
176
293
  return f"OpenAI API error: {str(e)}", "API_ERROR"
177
294
 
178
- # ─── handle model output (unchanged) ────────────────────────────────
179
- if completion and completion.output and len(completion.output) > 0:
180
- raw_text = None
181
- for out in completion.output:
182
- if out.type == "message" and out.content:
183
- for content_item in out.content:
184
- if hasattr(content_item, "text"):
185
- raw_text = content_item.text
186
- break
187
- else:
188
- logging.warning("request refused: %s", str(content_item))
189
- return "Request refused.", "FAIL"
190
- if raw_text:
191
- break
192
-
193
- if not raw_text or not raw_text.strip():
194
- return "No text returned (possibly refusal or empty response)", "FAIL"
195
-
196
- try:
197
- parsed_obj = response_format.parse_raw(raw_text)
198
- cache_output_tools.cache_output(
199
- "get_structured_output_internal", cache_key, parsed_obj.json()
200
- )
201
- return parsed_obj, "SUCCESS"
202
-
203
- except Exception:
204
- logging.warning("ERROR: Could not parse JSON from model output.")
205
- try:
206
- fixed_json = repair_json(raw_text)
207
- parsed_obj = response_format.parse_raw(fixed_json)
208
- cache_output_tools.cache_output(
209
- "get_structured_output_internal", cache_key, parsed_obj.json()
210
- )
211
- return parsed_obj, "SUCCESS"
212
- except Exception as e2:
213
- logging.warning("JSON repair failed: %s", str(e2))
214
- return raw_text, "FAIL"
215
- else:
216
- return "No output returned", "FAIL"
295
+ # ─── handle model output ────────────────────────────────────────────
296
+ return _parse_completion_response(completion, response_format, cache_key)
217
297
 
218
298
  # Safety fallback: catch any OpenAI errors not caught by inner retry loop
219
299
  except OpenAIError as e:
@@ -226,6 +306,173 @@ async def get_structured_output_internal(
226
306
  return f"Unexpected error: {str(e)}", "ERROR"
227
307
 
228
308
 
309
+ async def _get_structured_output_with_web_search(
310
+ client_async,
311
+ prompt: str,
312
+ response_format: BaseModel,
313
+ json_schema_format: Dict,
314
+ model: str,
315
+ effort: str,
316
+ tool_config: Optional[List[Dict]],
317
+ cache_key: str,
318
+ ):
319
+ """
320
+ Handles structured output with web search using Google search and URL scraping tools.
321
+ Works with both OpenAI and Azure OpenAI.
322
+ """
323
+ tools = [SEARCH_GOOGLE_TOOL, FETCH_URL_CONTENT_TOOL]
324
+
325
+ system_content = (
326
+ "You are a helpful AI. Output JSON only.\n\n"
327
+ "Web Search Instructions:\n"
328
+ "- Use search_google to find relevant information on the web.\n"
329
+ "- Use fetch_url_content to read the full content of relevant URLs.\n"
330
+ "- After gathering information, provide your response in the required JSON format."
331
+ )
332
+
333
+ input_messages = [
334
+ {"role": "system", "content": system_content},
335
+ {"role": "user", "content": prompt},
336
+ ]
337
+
338
+ base_request = {
339
+ "input": input_messages,
340
+ "model": model,
341
+ "text": {"format": json_schema_format},
342
+ "tools": tools,
343
+ "store": False,
344
+ }
345
+
346
+ if model.startswith("o"):
347
+ base_request["reasoning"] = {"effort": effort}
348
+
349
+ max_tool_iterations = 10
350
+ tool_iteration = 0
351
+ completion = None
352
+
353
+ while tool_iteration < max_tool_iterations:
354
+ tool_iteration += 1
355
+
356
+ # Retry logic for rate limits
357
+ for attempt in range(2):
358
+ try:
359
+ completion = await client_async.responses.create(**base_request)
360
+ break
361
+ except (RateLimitError, OpenAIError) as e:
362
+ is_rl = (
363
+ isinstance(e, RateLimitError)
364
+ or getattr(e, "status_code", None) == 429
365
+ or "rate_limit" in str(e).lower()
366
+ )
367
+ if attempt == 0 and is_rl:
368
+ wait_time = 20 + random.uniform(0, 2.0)
369
+ logging.warning(f"Rate-limit hit (429). Waiting {wait_time:.2f}s then retrying.")
370
+ await asyncio.sleep(wait_time)
371
+ continue
372
+ logging.error(f"OpenAI API error: {e}")
373
+ raise HTTPException(status_code=502, detail="Error communicating with the OpenAI API.")
374
+
375
+ if not completion:
376
+ raise HTTPException(status_code=502, detail="OpenAI request failed.")
377
+
378
+ # Check for function tool calls in the response
379
+ tool_calls = []
380
+ for item in (completion.output or []):
381
+ item_type = getattr(item, "type", None)
382
+ if item_type == "function_call":
383
+ tool_calls.append(item)
384
+
385
+ if not tool_calls:
386
+ # No tool calls, we have the final response
387
+ break
388
+
389
+ # Execute tool calls and prepare for next iteration
390
+ logging.info(f"Processing {len(tool_calls)} web search tool call(s) in iteration {tool_iteration}")
391
+
392
+ # Use previous_response_id for conversation threading
393
+ if hasattr(completion, "id") and completion.id:
394
+ base_request = {
395
+ "model": model,
396
+ "text": {"format": json_schema_format},
397
+ "tools": tools,
398
+ "store": False,
399
+ "previous_response_id": completion.id,
400
+ "input": [],
401
+ }
402
+ if model.startswith("o"):
403
+ base_request["reasoning"] = {"effort": effort}
404
+
405
+ for tc in tool_calls:
406
+ func_name = getattr(tc, "name", "")
407
+ call_id = getattr(tc, "call_id", "")
408
+ args_str = getattr(tc, "arguments", "{}")
409
+
410
+ try:
411
+ args = json.loads(args_str) if args_str else {}
412
+ except json.JSONDecodeError:
413
+ args = {}
414
+
415
+ # Execute the tool
416
+ tool_result = await _execute_web_search_tool(func_name, args, tool_config)
417
+
418
+ # Add tool result to input
419
+ base_request["input"].append({
420
+ "type": "function_call_output",
421
+ "call_id": call_id,
422
+ "output": tool_result
423
+ })
424
+
425
+ logging.info(f"Executed web search tool {func_name}, result length: {len(tool_result)}")
426
+ else:
427
+ logging.warning("No response ID available, breaking tool call loop")
428
+ break
429
+
430
+ # Parse and return the final response
431
+ return _parse_completion_response(completion, response_format, cache_key)
432
+
433
+
434
+ def _parse_completion_response(completion, response_format: BaseModel, cache_key: str):
435
+ """Parse completion response and return structured output."""
436
+ if completion and completion.output and len(completion.output) > 0:
437
+ raw_text = None
438
+ for out in completion.output:
439
+ if out.type == "message" and out.content:
440
+ for content_item in out.content:
441
+ if hasattr(content_item, "text"):
442
+ raw_text = content_item.text
443
+ break
444
+ else:
445
+ logging.warning("request refused: %s", str(content_item))
446
+ return "Request refused.", "FAIL"
447
+ if raw_text:
448
+ break
449
+
450
+ if not raw_text or not raw_text.strip():
451
+ return "No text returned (possibly refusal or empty response)", "FAIL"
452
+
453
+ try:
454
+ parsed_obj = response_format.parse_raw(raw_text)
455
+ cache_output_tools.cache_output(
456
+ "get_structured_output_internal", cache_key, parsed_obj.json()
457
+ )
458
+ return parsed_obj, "SUCCESS"
459
+
460
+ except Exception:
461
+ logging.warning("ERROR: Could not parse JSON from model output.")
462
+ try:
463
+ fixed_json = repair_json(raw_text)
464
+ parsed_obj = response_format.parse_raw(fixed_json)
465
+ cache_output_tools.cache_output(
466
+ "get_structured_output_internal", cache_key, parsed_obj.json()
467
+ )
468
+ return parsed_obj, "SUCCESS"
469
+ except Exception as e2:
470
+ logging.warning("JSON repair failed: %s", str(e2))
471
+ return raw_text, "FAIL"
472
+ else:
473
+ return "No output returned", "FAIL"
474
+
475
+
229
476
 
230
477
  async def get_structured_output_with_mcp(
231
478
  prompt: str,
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import logging
3
2
  import os
4
3
  from typing import Optional, List, Dict
@@ -21,7 +20,7 @@ def get_mailgun_notify_key(tool_config: Optional[List[Dict]] = None) -> str:
21
20
  if tool_config:
22
21
  cfg = next((item for item in tool_config if item.get("name") == "mailgun"), None)
23
22
  if cfg:
24
- cfg_map = {i["name"]: i["value"] for i in cfg.get("configuration", []) if i}
23
+ cfg_map = {i.get("name"): i.get("value") for i in cfg.get("configuration", []) if i}
25
24
  key = cfg_map.get("apiKey")
26
25
  key = key or os.getenv("MAILGUN_NOTIFY_KEY")
27
26
  if not key:
@@ -43,7 +42,7 @@ def get_mailgun_notify_domain(tool_config: Optional[List[Dict]] = None) -> str:
43
42
  if tool_config:
44
43
  cfg = next((item for item in tool_config if item.get("name") == "mailgun"), None)
45
44
  if cfg:
46
- cfg_map = {i["name"]: i["value"] for i in cfg.get("configuration", []) if i}
45
+ cfg_map = {i.get("name"): i.get("value") for i in cfg.get("configuration", []) if i}
47
46
  domain = cfg_map.get("domain") or cfg_map.get("notifyDomain")
48
47
  domain = domain or os.getenv("MAILGUN_DOMAIN") or os.getenv("MAILGUN_NOTIFY_DOMAIN")
49
48
  if not domain:
@@ -96,11 +95,15 @@ async def send_email_with_mailgun(
96
95
  # Try to return JSON payload if available
97
96
  try:
98
97
  return await response.json()
99
- except Exception:
98
+ except Exception as parse_ex:
99
+ logging.debug(f"Could not parse Mailgun response as JSON: {parse_ex}")
100
100
  return await response.text()
101
- except Exception as ex:
101
+ except (aiohttp.ClientError, ValueError) as ex:
102
102
  logging.warning(f"Error sending email via Mailgun: {ex}")
103
103
  return {"error": str(ex)}
104
+ except Exception as ex:
105
+ logging.exception(f"Unexpected error sending email via Mailgun: {ex}")
106
+ raise
104
107
 
105
108
 
106
109
  async def send_email_using_mailgun_async(
@@ -147,10 +150,10 @@ async def send_email_using_mailgun_async(
147
150
  raise RuntimeError(f"Mailgun send failed: {detail}")
148
151
  try:
149
152
  payload = await response.json()
150
- except Exception:
153
+ except Exception as parse_ex:
154
+ logging.debug(f"Could not parse Mailgun response as JSON: {parse_ex}")
151
155
  payload = {"message": await response.text()}
152
156
 
153
157
  # Normalise return value akin to other providers
154
158
  msg_id = payload.get("id") if isinstance(payload, dict) else None
155
- await asyncio.sleep(20)
156
159
  return msg_id or str(payload)
@@ -119,13 +119,17 @@ async def send_email_with_sendgrid(
119
119
  headers=headers,
120
120
  json=payload,
121
121
  ) as response:
122
- # SendGrid returns 202 Accepted on success with empty body
122
+ # SendGrid returns 202 Accepted on success with empty body but includes X-Message-Id header
123
123
  if response.status == 202:
124
- return {"status": 202, "message": "accepted"}
124
+ message_id = response.headers.get("X-Message-Id")
125
+ if not message_id:
126
+ logging.warning("SendGrid did not return X-Message-Id header")
127
+ return {"status": 202, "message": "accepted", "message_id": message_id}
125
128
  # On error, try to parse JSON for helpful message
126
129
  try:
127
130
  err = await response.json()
128
- except Exception:
131
+ except Exception as parse_ex:
132
+ logging.debug(f"Could not parse SendGrid error as JSON: {parse_ex}")
129
133
  err = {"text": await response.text()}
130
134
  return {"error": err, "status": response.status}
131
135
  except Exception as ex:
@@ -139,12 +143,8 @@ async def send_email_using_sendgrid_async(
139
143
  ) -> str:
140
144
  """
141
145
  Provider-style wrapper for SendGrid using SendEmailContext.
142
- Returns an opaque token since SendGrid does not return a message id.
146
+ Returns the message ID from SendGrid's X-Message-Id response header.
143
147
  """
144
- plain_body, html_body, _ = body_variants(
145
- ctx.body,
146
- getattr(ctx, "body_format", None),
147
- )
148
148
  result = await send_email_with_sendgrid(
149
149
  sender=f"{ctx.sender_name} <{ctx.sender_email}>",
150
150
  recipients=[ctx.recipient],
@@ -156,6 +156,10 @@ async def send_email_using_sendgrid_async(
156
156
  )
157
157
  # Normalise output to a string id-like value
158
158
  if isinstance(result, dict) and result.get("status") == 202:
159
+ message_id = result.get("message_id")
160
+ if message_id:
161
+ return message_id
162
+ # Fallback if header wasn't present (shouldn't happen)
159
163
  return f"sent:{ctx.sender_email}:{ctx.recipient}:{ctx.subject}"
160
164
  if isinstance(result, dict) and "error" in result:
161
165
  raise RuntimeError(f"SendGrid send failed: {result['error']}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev265
3
+ Version: 0.0.1.dev267
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
File without changes
File without changes