dhisana 0.0.1.dev307__tar.gz → 0.0.1.dev309__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.dev307 → dhisana-0.0.1.dev309}/PKG-INFO +2 -1
  2. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/setup.py +2 -1
  3. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/apollo_tools.py +38 -10
  4. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/test_connect.py +167 -1
  5. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana.egg-info/PKG-INFO +2 -1
  6. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana.egg-info/SOURCES.txt +1 -0
  7. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana.egg-info/requires.txt +1 -0
  8. dhisana-0.0.1.dev309/tests/test_apollo_json_error_handling.py +137 -0
  9. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/README.md +0 -0
  10. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/pyproject.toml +0 -0
  11. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/setup.cfg +0 -0
  12. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/__init__.py +0 -0
  13. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/cli/__init__.py +0 -0
  14. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/cli/cli.py +0 -0
  15. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/cli/datasets.py +0 -0
  16. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/cli/models.py +0 -0
  17. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/cli/predictions.py +0 -0
  18. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/schemas/__init__.py +0 -0
  19. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/schemas/common.py +0 -0
  20. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/schemas/sales.py +0 -0
  21. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/ui/__init__.py +0 -0
  22. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/ui/components.py +0 -0
  23. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/__init__.py +0 -0
  24. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/add_mapping.py +0 -0
  25. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/agent_tools.py +0 -0
  26. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/assistant_tool_tag.py +0 -0
  27. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/built_with_api_tools.py +0 -0
  28. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/cache_output_tools.py +0 -0
  29. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/cache_output_tools_local.py +0 -0
  30. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/check_email_validity_tools.py +0 -0
  31. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/check_for_intent_signal.py +0 -0
  32. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
  33. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/clay_tools.py +0 -0
  34. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/clean_properties.py +0 -0
  35. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/company_utils.py +0 -0
  36. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/compose_salesnav_query.py +0 -0
  37. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/compose_search_query.py +0 -0
  38. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
  39. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/composite_tools.py +0 -0
  40. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/dataframe_tools.py +0 -0
  41. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/domain_parser.py +0 -0
  42. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/email_body_utils.py +0 -0
  43. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/email_parse_helpers.py +0 -0
  44. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/email_provider.py +0 -0
  45. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/enrich_lead_information.py +0 -0
  46. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
  47. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/fetch_openai_config.py +0 -0
  48. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/field_validators.py +0 -0
  49. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/g2_tools.py +0 -0
  50. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_content.py +0 -0
  51. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_custom_message.py +0 -0
  52. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_email.py +0 -0
  53. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_email_response.py +0 -0
  54. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_flow.py +0 -0
  55. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
  56. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
  57. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
  58. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_sms_whatsapp.py +0 -0
  59. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
  60. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/google_custom_search.py +0 -0
  61. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/google_oauth_tools.py +0 -0
  62. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/google_workspace_tools.py +0 -0
  63. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/hubspot_clearbit.py +0 -0
  64. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
  65. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/instantly_tools.py +0 -0
  66. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/linkedin_crawler.py +0 -0
  67. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/lusha_tools.py +0 -0
  68. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/mailgun_tools.py +0 -0
  69. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/mailreach_tools.py +0 -0
  70. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/microsoft365_tools.py +0 -0
  71. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
  72. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openai_helpers.py +0 -0
  73. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
  74. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
  75. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
  76. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
  77. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
  78. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
  79. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/profile.py +0 -0
  80. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/proxy_curl_tools.py +0 -0
  81. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
  82. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/python_function_to_tools.py +0 -0
  83. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/research_lead.py +0 -0
  84. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
  85. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
  86. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/search_router.py +0 -0
  87. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/search_router_jobs.py +0 -0
  88. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/sendgrid_tools.py +0 -0
  89. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serarch_router_local_business.py +0 -0
  90. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
  91. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
  92. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serpapi_google_search.py +0 -0
  93. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
  94. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serpapi_search_tools.py +0 -0
  95. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
  96. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serperdev_local_business.py +0 -0
  97. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/serperdev_search.py +0 -0
  98. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/smtp_email_tools.py +0 -0
  99. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/trasform_json.py +0 -0
  100. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/web_download_parse_tools.py +0 -0
  101. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/workflow_code_model.py +0 -0
  102. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/utils/zoominfo_tools.py +0 -0
  103. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/workflow/__init__.py +0 -0
  104. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/workflow/agent.py +0 -0
  105. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/workflow/flow.py +0 -0
  106. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/workflow/task.py +0 -0
  107. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana/workflow/test.py +0 -0
  108. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana.egg-info/dependency_links.txt +0 -0
  109. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana.egg-info/entry_points.txt +0 -0
  110. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/src/dhisana.egg-info/top_level.txt +0 -0
  111. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_agent_tools.py +0 -0
  112. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_apollo_company_search.py +0 -0
  113. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_apollo_lead_search.py +0 -0
  114. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_connectivity.py +0 -0
  115. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_email_body_utils.py +0 -0
  116. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_generate_email.py +0 -0
  117. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_generate_sms_whatsapp.py +0 -0
  118. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_google_document.py +0 -0
  119. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_hubspot_call_logs.py +0 -0
  120. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_linkedin_serper.py +0 -0
  121. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_mailreach.py +0 -0
  122. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_mcp_connectivity.py +0 -0
  123. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_normalize_graph_datetime.py +0 -0
  124. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_proxycurl_get_company_search_id.py +0 -0
  125. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_proxycurl_job_count.py +0 -0
  126. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_reply_thread_fallback.py +0 -0
  127. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/tests/test_send_email_recipients.py +0 -0
  128. {dhisana-0.0.1.dev307 → dhisana-0.0.1.dev309}/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.dev307
3
+ Version: 0.0.1.dev309
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-dev307',
5
+ version='0.0.1-dev309',
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:
@@ -3,7 +3,7 @@ import logging
3
3
  import asyncio
4
4
  import os
5
5
  from datetime import datetime, timedelta, timezone
6
- from typing import Awaitable, Callable, Dict, List, Any, Optional
6
+ from typing import Awaitable, Callable, Dict, List, Any, Optional, Union
7
7
  import requests
8
8
 
9
9
  try:
@@ -2105,6 +2105,135 @@ 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: Union[str, Dict[str, Any]] = "",
2110
+ use_vertex_ai: bool = False,
2111
+ project: str = "",
2112
+ location: str = "us-central1",
2113
+ ) -> Dict[str, Any]:
2114
+ """Verify Gemini connectivity by listing available models.
2115
+
2116
+ Args:
2117
+ api_key: A Gemini API key, a service-account JSON string, or an
2118
+ already-parsed service-account ``dict``. When a service-account
2119
+ JSON is detected, Vertex AI mode is 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
+ # Normalise api_key: it may arrive as a dict (already-parsed
2135
+ # service-account JSON from Pydantic config deserialization).
2136
+ if isinstance(api_key, dict):
2137
+ if api_key.get("type") == "service_account":
2138
+ sa_info = api_key
2139
+ else:
2140
+ return {
2141
+ "success": False,
2142
+ "status_code": 0,
2143
+ "error_message": "api_key is a dict but not a valid service account JSON (missing \"type\": \"service_account\").",
2144
+ }
2145
+ api_key = "" # not a plain API key string
2146
+ else:
2147
+ # Try to parse service account JSON from the api_key field
2148
+ sa_info = None
2149
+ if api_key and api_key.strip().startswith("{"):
2150
+ try:
2151
+ parsed = json.loads(api_key)
2152
+ if isinstance(parsed, dict) and parsed.get("type") == "service_account":
2153
+ sa_info = parsed
2154
+ except (json.JSONDecodeError, TypeError):
2155
+ pass
2156
+
2157
+ # Validate required fields in service-account JSON
2158
+ if sa_info:
2159
+ missing_sa_fields = [
2160
+ f for f in ("private_key", "client_email")
2161
+ if not sa_info.get(f)
2162
+ ]
2163
+ if missing_sa_fields:
2164
+ return {
2165
+ "success": False,
2166
+ "status_code": 0,
2167
+ "error_message": f"Service account JSON is missing required fields: {', '.join(missing_sa_fields)}",
2168
+ }
2169
+
2170
+ # Auto-detect: service account JSON → force Vertex AI
2171
+ if sa_info and not use_vertex_ai:
2172
+ use_vertex_ai = True
2173
+ if not project:
2174
+ project = sa_info.get("project_id", "")
2175
+
2176
+ if use_vertex_ai and project:
2177
+ if sa_info:
2178
+ from google.oauth2 import service_account as sa_module
2179
+ credentials = sa_module.Credentials.from_service_account_info(
2180
+ sa_info,
2181
+ scopes=["https://www.googleapis.com/auth/cloud-platform"],
2182
+ )
2183
+ client = genai.Client(
2184
+ vertexai=True,
2185
+ project=project,
2186
+ location=location,
2187
+ credentials=credentials,
2188
+ )
2189
+ else:
2190
+ client = genai.Client(
2191
+ vertexai=True,
2192
+ project=project,
2193
+ location=location,
2194
+ )
2195
+ elif api_key and not sa_info:
2196
+ client = genai.Client(api_key=api_key)
2197
+ elif sa_info:
2198
+ return {
2199
+ "success": False,
2200
+ "status_code": 0,
2201
+ "error_message": "API key contains a service account JSON but no GCP project could be determined. "
2202
+ "Set the 'GCP Project ID' field or ensure the JSON includes 'project_id'.",
2203
+ }
2204
+ else:
2205
+ return {
2206
+ "success": False,
2207
+ "status_code": 0,
2208
+ "error_message": "No API key or Vertex AI project configured.",
2209
+ }
2210
+
2211
+ # Use models.list to verify credentials without depending on a
2212
+ # specific model being available in the configured location.
2213
+ found_any = False
2214
+ models_iter = await client.aio.models.list(config={"page_size": 1})
2215
+ async for _ in models_iter:
2216
+ found_any = True
2217
+ break
2218
+ if not found_any:
2219
+ return {
2220
+ "success": False,
2221
+ "status_code": 0,
2222
+ "error_message": "Authentication succeeded but no models are accessible. "
2223
+ "Check your GCP project permissions or API key.",
2224
+ }
2225
+ # Any successful API response proves connectivity, regardless of
2226
+ # whether the model returned text (safety filters may suppress it).
2227
+ return {"success": True, "status_code": 200, "error_message": None}
2228
+ except Exception as exc:
2229
+ err_msg = str(exc)
2230
+ # Avoid leaking credentials in error messages
2231
+ if isinstance(api_key, str) and api_key and api_key in err_msg:
2232
+ err_msg = err_msg.replace(api_key, "<REDACTED>")
2233
+ logger.error(f"Gemini connectivity test failed: {err_msg}")
2234
+ return {"success": False, "status_code": 0, "error_message": err_msg}
2235
+
2236
+
2108
2237
  ###############################################################################
2109
2238
  # MAIN CONNECTIVITY FUNCTION
2110
2239
  ###############################################################################
@@ -2163,6 +2292,7 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
2163
2292
  "sendgrid": test_sendgrid,
2164
2293
  "samgov": test_samgov,
2165
2294
  "scraperapi": test_scraperapi,
2295
+ "gemini": test_gemini,
2166
2296
  }
2167
2297
 
2168
2298
  results: Dict[str, Dict[str, Any]] = {}
@@ -2499,6 +2629,42 @@ async def test_connectivity(tool_config: List[Dict[str, Any]]) -> Dict[str, Dict
2499
2629
  )
2500
2630
  continue
2501
2631
 
2632
+ # ------------------------------------------------------------------ #
2633
+ # Special-case: Gemini (api_key or Vertex AI)
2634
+ # ------------------------------------------------------------------ #
2635
+ if tool_name == "gemini":
2636
+ _MISSING = object()
2637
+
2638
+ def _gem_first(*names, default=None):
2639
+ for n in names:
2640
+ val = next((c["value"] for c in config_entries if c["name"] == n), _MISSING)
2641
+ if val is not _MISSING and val:
2642
+ return val
2643
+ return default
2644
+
2645
+ gem_api_key = _gem_first("api_key", "apiKey", "apikey", default="")
2646
+ use_vertex = str(
2647
+ _gem_first("use_vertex_ai", "vertexai", default="false")
2648
+ ).lower() in ("true", "1", "yes")
2649
+ gem_project = _gem_first("project", "gcp_project", default="")
2650
+ gem_location = _gem_first("location", "gcp_location", default="us-central1")
2651
+
2652
+ if not gem_api_key and not (use_vertex and gem_project):
2653
+ results[tool_name] = {
2654
+ "success": False,
2655
+ "status_code": 0,
2656
+ "error_message": "Provide either an API key or enable Vertex AI with a GCP project ID.",
2657
+ }
2658
+ else:
2659
+ logger.info("Testing connectivity for Gemini…")
2660
+ results[tool_name] = await test_gemini(
2661
+ api_key=gem_api_key,
2662
+ use_vertex_ai=use_vertex,
2663
+ project=gem_project,
2664
+ location=gem_location,
2665
+ )
2666
+ continue
2667
+
2502
2668
  # ------------------------------------------------------------------ #
2503
2669
  # All other tools – expect an apiKey by default
2504
2670
  # ------------------------------------------------------------------ #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dhisana
3
- Version: 0.0.1.dev307
3
+ Version: 0.0.1.dev309
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