dhisana 0.0.1.dev306__tar.gz → 0.0.1.dev308__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 (128) hide show
  1. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/PKG-INFO +2 -1
  2. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/setup.py +2 -1
  3. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/apollo_tools.py +38 -10
  4. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_sms_whatsapp.py +10 -3
  5. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/test_connect.py +143 -0
  6. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/PKG-INFO +2 -1
  7. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/SOURCES.txt +1 -0
  8. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/requires.txt +1 -0
  9. dhisana-0.0.1.dev308/tests/test_apollo_json_error_handling.py +137 -0
  10. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/README.md +0 -0
  11. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/pyproject.toml +0 -0
  12. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/setup.cfg +0 -0
  13. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/__init__.py +0 -0
  14. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/__init__.py +0 -0
  15. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/cli.py +0 -0
  16. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/datasets.py +0 -0
  17. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/models.py +0 -0
  18. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/predictions.py +0 -0
  19. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/schemas/__init__.py +0 -0
  20. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/schemas/common.py +0 -0
  21. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/schemas/sales.py +0 -0
  22. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/ui/__init__.py +0 -0
  23. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/ui/components.py +0 -0
  24. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/__init__.py +0 -0
  25. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/add_mapping.py +0 -0
  26. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/agent_tools.py +0 -0
  27. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  28. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/built_with_api_tools.py +0 -0
  29. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/cache_output_tools.py +0 -0
  30. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  31. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  32. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  33. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  34. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/clay_tools.py +0 -0
  35. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/clean_properties.py +0 -0
  36. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/company_utils.py +0 -0
  37. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  38. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_search_query.py +0 -0
  39. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  40. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/composite_tools.py +0 -0
  41. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/dataframe_tools.py +0 -0
  42. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/domain_parser.py +0 -0
  43. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/email_body_utils.py +0 -0
  44. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/email_parse_helpers.py +0 -0
  45. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/email_provider.py +0 -0
  46. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/enrich_lead_information.py +0 -0
  47. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  48. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/fetch_openai_config.py +0 -0
  49. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/field_validators.py +0 -0
  50. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/g2_tools.py +0 -0
  51. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_content.py +0 -0
  52. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_custom_message.py +0 -0
  53. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_email.py +0 -0
  54. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_email_response.py +0 -0
  55. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_flow.py +0 -0
  56. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  57. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  58. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  59. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  60. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/google_custom_search.py +0 -0
  61. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/google_oauth_tools.py +0 -0
  62. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/google_workspace_tools.py +0 -0
  63. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  64. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  65. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/instantly_tools.py +0 -0
  66. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/linkedin_crawler.py +0 -0
  67. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/lusha_tools.py +0 -0
  68. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/mailgun_tools.py +0 -0
  69. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/mailreach_tools.py +0 -0
  70. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/microsoft365_tools.py +0 -0
  71. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  72. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openai_helpers.py +0 -0
  73. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  74. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  75. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  76. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  77. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  78. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  79. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/profile.py +0 -0
  80. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  81. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  82. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/python_function_to_tools.py +0 -0
  83. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/research_lead.py +0 -0
  84. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  85. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  86. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/search_router.py +0 -0
  87. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/search_router_jobs.py +0 -0
  88. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/sendgrid_tools.py +0 -0
  89. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  90. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  91. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  92. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_google_search.py +0 -0
  93. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  94. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  95. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  96. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serperdev_local_business.py +0 -0
  97. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serperdev_search.py +0 -0
  98. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/smtp_email_tools.py +0 -0
  99. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/trasform_json.py +0 -0
  100. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  101. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/workflow_code_model.py +0 -0
  102. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/zoominfo_tools.py +0 -0
  103. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/__init__.py +0 -0
  104. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/agent.py +0 -0
  105. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/flow.py +0 -0
  106. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/task.py +0 -0
  107. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/test.py +0 -0
  108. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/dependency_links.txt +0 -0
  109. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/entry_points.txt +0 -0
  110. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/top_level.txt +0 -0
  111. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_agent_tools.py +0 -0
  112. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_apollo_company_search.py +0 -0
  113. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_apollo_lead_search.py +0 -0
  114. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_connectivity.py +0 -0
  115. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_email_body_utils.py +0 -0
  116. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_generate_email.py +0 -0
  117. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_generate_sms_whatsapp.py +0 -0
  118. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_google_document.py +0 -0
  119. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_hubspot_call_logs.py +0 -0
  120. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_linkedin_serper.py +0 -0
  121. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_mailreach.py +0 -0
  122. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_mcp_connectivity.py +0 -0
  123. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_normalize_graph_datetime.py +0 -0
  124. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_proxycurl_get_company_search_id.py +0 -0
  125. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_proxycurl_job_count.py +0 -0
  126. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_reply_thread_fallback.py +0 -0
  127. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_send_email_recipients.py +0 -0
  128. {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/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.dev306
3
+ Version: 0.0.1.dev308
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
@@ -16,6 +16,7 @@ Requires-Dist: click>=7.0
16
16
  Requires-Dist: fastapi
17
17
  Requires-Dist: google-api-python-client
18
18
  Requires-Dist: google-auth
19
+ Requires-Dist: google-genai>=1.0.0
19
20
  Requires-Dist: openai
20
21
  Requires-Dist: playwright
21
22
  Requires-Dist: requests
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='dhisana',
5
- version='0.0.1-dev306',
5
+ version='0.0.1-dev308',
6
6
  description='A Python SDK for Dhisana AI Platform',
7
7
  author='Admin',
8
8
  author_email='contact@dhisana.ai',
@@ -16,6 +16,7 @@ setup(
16
16
  'fastapi',
17
17
  'google-api-python-client',
18
18
  'google-auth',
19
+ 'google-genai>=1.0.0',
19
20
  'openai',
20
21
  'playwright',
21
22
  'requests',
@@ -201,9 +201,15 @@ async def enrich_person_info_from_apollo(
201
201
  headers=response.headers
202
202
  )
203
203
  else:
204
- result = await response.json()
205
- logger.warning(f"enrich_person_info_from_apollo error: {result}")
204
+ body = await response.text()
205
+ try:
206
+ result = json.loads(body)
207
+ except (json.JSONDecodeError, ValueError):
208
+ result = body or f"HTTP {response.status}"
209
+ logger.warning(f"enrich_person_info_from_apollo error (status={response.status}): {str(result)[:500]}")
206
210
  return {'error': result}
211
+ except aiohttp.ClientResponseError:
212
+ raise
207
213
  except Exception as e:
208
214
  logger.exception("Exception occurred while fetching person info from Apollo.")
209
215
  return {'error': str(e)}
@@ -281,9 +287,15 @@ async def lookup_person_in_apollo_by_name(
281
287
  headers=response.headers
282
288
  )
283
289
  else:
284
- result = await response.json()
285
- logger.warning(f"lookup_person_in_apollo_by_name error: {result}")
290
+ body = await response.text()
291
+ try:
292
+ result = json.loads(body)
293
+ except (json.JSONDecodeError, ValueError):
294
+ result = body or f"HTTP {response.status}"
295
+ logger.warning(f"lookup_person_in_apollo_by_name error (status={response.status}): {str(result)[:500]}")
286
296
  return {'error': result}
297
+ except aiohttp.ClientResponseError:
298
+ raise
287
299
  except Exception as e:
288
300
  logger.exception("Exception occurred while looking up person by name.")
289
301
  return {'error': str(e)}
@@ -371,9 +383,15 @@ async def enrich_organization_info_from_apollo(
371
383
  headers=response.headers
372
384
  )
373
385
  else:
374
- result = await response.json()
375
- logger.warning(f"Error from Apollo while enriching org info: {result}")
386
+ body = await response.text()
387
+ try:
388
+ result = json.loads(body)
389
+ except (json.JSONDecodeError, ValueError):
390
+ result = body or f"HTTP {response.status}"
391
+ logger.warning(f"Error from Apollo while enriching org info (status={response.status}): {str(result)[:500]}")
376
392
  return {'error': result}
393
+ except aiohttp.ClientResponseError:
394
+ raise
377
395
  except Exception as e:
378
396
  logger.exception("Exception occurred while fetching organization info from Apollo.")
379
397
  return {'error': str(e)}
@@ -1132,9 +1150,15 @@ async def get_organization_details_from_apollo(
1132
1150
  headers=response.headers
1133
1151
  )
1134
1152
  else:
1135
- result = await response.json()
1136
- logger.warning(f"get_organization_details_from_apollo error: {result}")
1153
+ body = await response.text()
1154
+ try:
1155
+ result = json.loads(body)
1156
+ except (json.JSONDecodeError, ValueError):
1157
+ result = body or f"HTTP {response.status}"
1158
+ logger.warning(f"get_organization_details_from_apollo error (status={response.status}): {str(result)[:500]}")
1137
1159
  return {'error': result}
1160
+ except aiohttp.ClientResponseError:
1161
+ raise
1138
1162
  except Exception as e:
1139
1163
  logger.exception("Exception occurred while fetching organization details from Apollo.")
1140
1164
  return {'error': str(e)}
@@ -2219,8 +2243,12 @@ async def search_organization_by_linkedin_or_domain(
2219
2243
  headers=response.headers
2220
2244
  )
2221
2245
  else:
2222
- result = await response.json()
2223
- logger.warning(f"search_organization_by_linkedin_or_domain error: {result}")
2246
+ body = await response.text()
2247
+ try:
2248
+ result = json.loads(body)
2249
+ except (json.JSONDecodeError, ValueError):
2250
+ result = body or f"HTTP {response.status}"
2251
+ logger.warning(f"search_organization_by_linkedin_or_domain error (status={response.status}): {str(result)[:500]}")
2224
2252
  return {'error': result}
2225
2253
 
2226
2254
  except aiohttp.ClientResponseError:
@@ -101,7 +101,9 @@ async def generate_sms_whatsapp_copy(
101
101
  usable_chars = char_limit - len(opt_out_suffix)
102
102
  channel_label = "WhatsApp" if channel_type == ChannelType.WHATSAPP else "SMS"
103
103
 
104
- prompt = f"""You are an AI sales copywriter. Write exactly one personalized {channel_label} message on behalf of the sender to the lead described below.
104
+ prompt = f"""You are an AI sales copywriter. Your task is to write exactly one personalized outbound business {channel_label} text message on behalf of the sender to the lead described below.
105
+
106
+ This is a legitimate B2B sales outreach message. The sender has opted in to sending these messages through our platform.
105
107
 
106
108
  ## Channel Rules — {channel_label}
107
109
 
@@ -111,8 +113,9 @@ async def generate_sms_whatsapp_copy(
111
113
  4. Be concise and conversational. Lead with value, not a greeting. Avoid filler.
112
114
  5. Do NOT include any opt-out / STOP language — the system appends it automatically.
113
115
  6. Do NOT include phone numbers, email addresses, or links unless the writing instructions explicitly request a specific provided URL.
114
- 7. Ground every claim in the provided context. Never fabricate information.
116
+ 7. Ground every claim in the provided context. When information is missing, omit it — never guess or fabricate.
115
117
  8. Do not use em dashes.
118
+ 9. The "body" field must contain ONLY the message text. Never append JSON, metadata, or structured data to the body.
116
119
 
117
120
  ## Lead
118
121
 
@@ -187,8 +190,12 @@ Return JSON with a single field: "body" (string, plain text, max {usable_chars}
187
190
  )
188
191
 
189
192
  if status != "SUCCESS":
193
+ # Include the refusal/error detail if the model returned text
194
+ detail = ""
195
+ if isinstance(response, str) and response:
196
+ detail = f" Model response: {response[:200]}"
190
197
  raise Exception(
191
- f"Error: Could not generate {channel_label} message."
198
+ f"Error: Could not generate {channel_label} message.{detail}"
192
199
  )
193
200
 
194
201
  body = (response.body or "").strip()
@@ -2105,6 +2105,112 @@ async def test_dataverse(
2105
2105
  return {"success": False, "status_code": 0, "error_message": str(exc)}
2106
2106
 
2107
2107
 
2108
+ async def test_gemini(
2109
+ api_key: str = "",
2110
+ use_vertex_ai: bool = False,
2111
+ project: str = "",
2112
+ location: str = "us-central1",
2113
+ ) -> Dict[str, Any]:
2114
+ """Verify Gemini connectivity by issuing a minimal generate_content call.
2115
+
2116
+ Args:
2117
+ api_key: A Gemini API key *or* a service-account JSON string.
2118
+ When a service-account JSON is detected, Vertex AI mode is
2119
+ enabled automatically.
2120
+ use_vertex_ai: Explicitly enable Vertex AI mode.
2121
+ project: GCP project ID (required for Vertex AI).
2122
+ location: GCP region for Vertex AI (default ``us-central1``).
2123
+ """
2124
+ try:
2125
+ from google import genai
2126
+ except ImportError:
2127
+ return {
2128
+ "success": False,
2129
+ "status_code": 0,
2130
+ "error_message": "google-genai package is not installed. Install with: pip install google-genai",
2131
+ }
2132
+
2133
+ try:
2134
+ # Try to parse service account JSON from the api_key field
2135
+ sa_info = None
2136
+ if api_key and api_key.strip().startswith("{"):
2137
+ try:
2138
+ parsed = json.loads(api_key)
2139
+ if isinstance(parsed, dict) and parsed.get("type") == "service_account":
2140
+ sa_info = parsed
2141
+ except (json.JSONDecodeError, TypeError):
2142
+ pass
2143
+
2144
+ # Validate required fields in service-account JSON
2145
+ if sa_info:
2146
+ missing_sa_fields = [
2147
+ f for f in ("private_key", "client_email")
2148
+ if not sa_info.get(f)
2149
+ ]
2150
+ if missing_sa_fields:
2151
+ return {
2152
+ "success": False,
2153
+ "status_code": 0,
2154
+ "error_message": f"Service account JSON is missing required fields: {', '.join(missing_sa_fields)}",
2155
+ }
2156
+
2157
+ # Auto-detect: service account JSON → force Vertex AI
2158
+ if sa_info and not use_vertex_ai:
2159
+ use_vertex_ai = True
2160
+ if not project:
2161
+ project = sa_info.get("project_id", "")
2162
+
2163
+ if use_vertex_ai and project:
2164
+ if sa_info:
2165
+ from google.oauth2 import service_account as sa_module
2166
+ credentials = sa_module.Credentials.from_service_account_info(
2167
+ sa_info,
2168
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
2169
+ )
2170
+ client = genai.Client(
2171
+ vertexai=True,
2172
+ project=project,
2173
+ location=location,
2174
+ credentials=credentials,
2175
+ )
2176
+ else:
2177
+ client = genai.Client(
2178
+ vertexai=True,
2179
+ project=project,
2180
+ location=location,
2181
+ )
2182
+ elif api_key and not sa_info:
2183
+ client = genai.Client(api_key=api_key)
2184
+ elif sa_info:
2185
+ return {
2186
+ "success": False,
2187
+ "status_code": 0,
2188
+ "error_message": "API key contains a service account JSON but no GCP project could be determined. "
2189
+ "Set the 'GCP Project ID' field or ensure the JSON includes 'project_id'.",
2190
+ }
2191
+ else:
2192
+ return {
2193
+ "success": False,
2194
+ "status_code": 0,
2195
+ "error_message": "No API key or Vertex AI project configured.",
2196
+ }
2197
+
2198
+ response = await client.aio.models.generate_content(
2199
+ model="gemini-2.0-flash-lite",
2200
+ contents="Reply with exactly: OK",
2201
+ )
2202
+ # Any successful API response proves connectivity, regardless of
2203
+ # whether the model returned text (safety filters may suppress it).
2204
+ return {"success": True, "status_code": 200, "error_message": None}
2205
+ except Exception as exc:
2206
+ err_msg = str(exc)
2207
+ # Avoid leaking credentials in error messages
2208
+ if api_key and api_key in err_msg:
2209
+ err_msg = err_msg.replace(api_key, "<REDACTED>")
2210
+ logger.error(f"Gemini connectivity test failed: {err_msg}")
2211
+ return {"success": False, "status_code": 0, "error_message": err_msg}
2212
+
2213
+
2108
2214
  ###############################################################################
2109
2215
  # MAIN CONNECTIVITY FUNCTION
2110
2216
  ###############################################################################
@@ -2163,6 +2269,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
2163
2269
  "sendgrid": test_sendgrid,
2164
2270
  "samgov": test_samgov,
2165
2271
  "scraperapi": test_scraperapi,
2272
+ "gemini": test_gemini,
2166
2273
  }
2167
2274
 
2168
2275
  results: Dict[str, Dict[str, Any]] = {}
@@ -2499,6 +2606,42 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
2499
2606
  )
2500
2607
  continue
2501
2608
 
2609
+ # ------------------------------------------------------------------ #
2610
+ # Special-case: Gemini (api_key or Vertex AI)
2611
+ # ------------------------------------------------------------------ #
2612
+ if tool_name == "gemini":
2613
+ _MISSING = object()
2614
+
2615
+ def _gem_first(*names, default=None):
2616
+ for n in names:
2617
+ val = next((c["value"] for c in config_entries if c["name"] == n), _MISSING)
2618
+ if val is not _MISSING and val:
2619
+ return val
2620
+ return default
2621
+
2622
+ gem_api_key = _gem_first("api_key", "apiKey", "apikey", default="")
2623
+ use_vertex = str(
2624
+ _gem_first("use_vertex_ai", "vertexai", default="false")
2625
+ ).lower() in ("true", "1", "yes")
2626
+ gem_project = _gem_first("project", "gcp_project", default="")
2627
+ gem_location = _gem_first("location", "gcp_location", default="us-central1")
2628
+
2629
+ if not gem_api_key and not (use_vertex and gem_project):
2630
+ results[tool_name] = {
2631
+ "success": False,
2632
+ "status_code": 0,
2633
+ "error_message": "Provide either an API key or enable Vertex AI with a GCP project ID.",
2634
+ }
2635
+ else:
2636
+ logger.info("Testing connectivity for Gemini…")
2637
+ results[tool_name] = await test_gemini(
2638
+ api_key=gem_api_key,
2639
+ use_vertex_ai=use_vertex,
2640
+ project=gem_project,
2641
+ location=gem_location,
2642
+ )
2643
+ continue
2644
+
2502
2645
  # ------------------------------------------------------------------ #
2503
2646
  # All other tools – expect an apiKey by default
2504
2647
  # ------------------------------------------------------------------ #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev306
3
+ Version: 0.0.1.dev308
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
@@ -16,6 +16,7 @@ Requires-Dist: click>=7.0
16
16
  Requires-Dist: fastapi
17
17
  Requires-Dist: google-api-python-client
18
18
  Requires-Dist: google-auth
19
+ Requires-Dist: google-genai>=1.0.0
19
20
  Requires-Dist: openai
20
21
  Requires-Dist: playwright
21
22
  Requires-Dist: requests
@@ -107,6 +107,7 @@ src/dhisana/workflow/task.py
107
107
  src/dhisana/workflow/test.py
108
108
  tests/test_agent_tools.py
109
109
  tests/test_apollo_company_search.py
110
+ tests/test_apollo_json_error_handling.py
110
111
  tests/test_apollo_lead_search.py
111
112
  tests/test_connectivity.py
112
113
  tests/test_email_body_utils.py
@@ -3,6 +3,7 @@ click>=7.0
3
3
  fastapi
4
4
  google-api-python-client
5
5
  google-auth
6
+ google-genai>=1.0.0
6
7
  openai
7
8
  playwright
8
9
  requests
@@ -0,0 +1,137 @@
1
+ """Tests for Apollo lookup/enrich – non-JSON response handling.
2
+
3
+ Reproduces DHISANA-STAGE-14H: JSONDecodeError when Apollo returns an empty
4
+ or non-JSON body on error status codes.
5
+ """
6
+
7
+ import json
8
+
9
+ import pytest
10
+ from unittest.mock import AsyncMock, MagicMock, patch
11
+
12
+
13
+ def _mock_aiohttp_session(method, response_status, response_body):
14
+ """Build a mock aiohttp.ClientSession whose *method* returns *response_body*."""
15
+ mock_response = MagicMock()
16
+ mock_response.status = response_status
17
+ mock_response.text = AsyncMock(return_value=response_body)
18
+ mock_response.request_info = MagicMock()
19
+ mock_response.history = ()
20
+ mock_response.headers = {}
21
+
22
+ mock_cm = MagicMock()
23
+ mock_cm.__aenter__ = AsyncMock(return_value=mock_response)
24
+ mock_cm.__aexit__ = AsyncMock(return_value=None)
25
+
26
+ mock_session = MagicMock()
27
+ setattr(mock_session, method, MagicMock(return_value=mock_cm))
28
+
29
+ mock_session_cm = MagicMock()
30
+ mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session)
31
+ mock_session_cm.__aexit__ = AsyncMock(return_value=None)
32
+
33
+ return mock_session_cm
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # lookup_person_in_apollo_by_name
38
+ # ---------------------------------------------------------------------------
39
+
40
+ class TestLookupPersonByName:
41
+
42
+ @pytest.mark.asyncio
43
+ @patch("src.dhisana.utils.apollo_tools.get_apollo_access_token", return_value=("fake-key", False))
44
+ async def test_empty_body_error_status(self, _mock_token):
45
+ """Non-200 with empty body should return error dict, not raise."""
46
+ from src.dhisana.utils.apollo_tools import lookup_person_in_apollo_by_name
47
+
48
+ with patch("aiohttp.ClientSession", return_value=_mock_aiohttp_session("post", 502, "")):
49
+ result = await lookup_person_in_apollo_by_name("Jane Doe")
50
+
51
+ assert "error" in result
52
+ assert "JSONDecodeError" not in str(result["error"])
53
+
54
+ @pytest.mark.asyncio
55
+ @patch("src.dhisana.utils.apollo_tools.get_apollo_access_token", return_value=("fake-key", False))
56
+ async def test_html_body_error_status(self, _mock_token):
57
+ """Non-200 with HTML body should return the HTML text as error."""
58
+ from src.dhisana.utils.apollo_tools import lookup_person_in_apollo_by_name
59
+
60
+ html = "<html><body>Service Unavailable</body></html>"
61
+ with patch("aiohttp.ClientSession", return_value=_mock_aiohttp_session("post", 503, html)):
62
+ result = await lookup_person_in_apollo_by_name("Jane Doe")
63
+
64
+ assert "error" in result
65
+ assert "Service Unavailable" in str(result["error"])
66
+
67
+ @pytest.mark.asyncio
68
+ @patch("src.dhisana.utils.apollo_tools.get_apollo_access_token", return_value=("fake-key", False))
69
+ async def test_json_error_body(self, _mock_token):
70
+ """Non-200 with valid JSON body should return parsed JSON as error."""
71
+ from src.dhisana.utils.apollo_tools import lookup_person_in_apollo_by_name
72
+
73
+ body = json.dumps({"message": "Unauthorized"})
74
+ with patch("aiohttp.ClientSession", return_value=_mock_aiohttp_session("post", 401, body)):
75
+ result = await lookup_person_in_apollo_by_name("Jane Doe")
76
+
77
+ assert result["error"] == {"message": "Unauthorized"}
78
+
79
+ @pytest.mark.asyncio
80
+ @patch("src.dhisana.utils.apollo_tools.get_apollo_access_token", return_value=("fake-key", False))
81
+ async def test_success(self, _mock_token):
82
+ """200 response returns parsed JSON body."""
83
+ from src.dhisana.utils.apollo_tools import lookup_person_in_apollo_by_name
84
+
85
+ body = json.dumps({"people": [{"name": "Jane Doe"}]})
86
+ with patch("aiohttp.ClientSession", return_value=_mock_aiohttp_session("post", 200, body)):
87
+ result = await lookup_person_in_apollo_by_name("Jane Doe")
88
+
89
+ assert result == {"people": [{"name": "Jane Doe"}]}
90
+
91
+ @pytest.mark.asyncio
92
+ async def test_empty_name(self):
93
+ """Empty full_name should return error without making API call."""
94
+ from src.dhisana.utils.apollo_tools import lookup_person_in_apollo_by_name
95
+
96
+ result = await lookup_person_in_apollo_by_name("")
97
+ assert "error" in result
98
+
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # enrich_person_info_from_apollo
102
+ # ---------------------------------------------------------------------------
103
+
104
+ class TestEnrichPersonInfo:
105
+
106
+ @pytest.mark.asyncio
107
+ @patch("src.dhisana.utils.apollo_tools.get_apollo_access_token", return_value=("fake-key", False))
108
+ async def test_empty_body_error_status(self, _mock_token):
109
+ from src.dhisana.utils.apollo_tools import enrich_person_info_from_apollo
110
+
111
+ with patch("aiohttp.ClientSession", return_value=_mock_aiohttp_session("post", 500, "")):
112
+ result = await enrich_person_info_from_apollo(
113
+ email="test@example.com",
114
+ )
115
+
116
+ assert "error" in result
117
+ assert "JSONDecodeError" not in str(result["error"])
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # enrich_organization_info_from_apollo
122
+ # ---------------------------------------------------------------------------
123
+
124
+ class TestEnrichOrgInfo:
125
+
126
+ @pytest.mark.asyncio
127
+ @patch("src.dhisana.utils.apollo_tools.get_apollo_access_token", return_value=("fake-key", False))
128
+ async def test_empty_body_error_status(self, _mock_token):
129
+ from src.dhisana.utils.apollo_tools import enrich_organization_info_from_apollo
130
+
131
+ with patch("aiohttp.ClientSession", return_value=_mock_aiohttp_session("get", 502, "")):
132
+ result = await enrich_organization_info_from_apollo(
133
+ organization_domain="example.com",
134
+ )
135
+
136
+ assert "error" in result
137
+ assert "JSONDecodeError" not in str(result["error"])
File without changes
File without changes