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.
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/PKG-INFO +2 -1
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/setup.py +2 -1
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/apollo_tools.py +38 -10
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_sms_whatsapp.py +10 -3
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/test_connect.py +143 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/PKG-INFO +2 -1
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/SOURCES.txt +1 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/requires.txt +1 -0
- dhisana-0.0.1.dev308/tests/test_apollo_json_error_handling.py +137 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/README.md +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/pyproject.toml +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/setup.cfg +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/cli.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/datasets.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/models.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/cli/predictions.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/schemas/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/schemas/common.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/schemas/sales.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/ui/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/ui/components.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/add_mapping.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/agent_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/assistant_tool_tag.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/built_with_api_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/cache_output_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/cache_output_tools_local.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_email_validity_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_for_intent_signal.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_linkedin_url_validity.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/clay_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/clean_properties.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/company_utils.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_salesnav_query.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_search_query.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_three_step_workflow.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/composite_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/dataframe_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/domain_parser.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/email_body_utils.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/email_parse_helpers.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/email_provider.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/enrich_lead_information.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/extract_email_content_for_llm.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/fetch_openai_config.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/field_validators.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/g2_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_content.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_custom_message.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_email.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_email_response.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_flow.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_leads_salesnav.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_linkedin_connect_message.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_linkedin_response_message.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_structured_output_internal.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/google_custom_search.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/google_oauth_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/google_workspace_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/hubspot_clearbit.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/hubspot_crm_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/instantly_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/linkedin_crawler.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/lusha_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/mailgun_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/mailreach_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/microsoft365_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openai_assistant_and_file_utils.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openai_helpers.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_spec_to_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/api_models.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/convert_openai_spec_to_tool.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/openapi_tool.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/parse_linkedin_messages_txt.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/profile.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/proxy_curl_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/proxycurl_search_leads.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/python_function_to_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/research_lead.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/sales_navigator_crawler.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/salesforce_crm_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/search_router.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/search_router_jobs.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/sendgrid_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serarch_router_local_business.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_additional_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_google_jobs.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_google_search.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_local_business_search.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_search_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serperdev_google_jobs.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serperdev_local_business.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serperdev_search.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/smtp_email_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/trasform_json.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/web_download_parse_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/workflow_code_model.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/zoominfo_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/__init__.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/agent.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/flow.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/task.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/workflow/test.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/dependency_links.txt +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/entry_points.txt +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana.egg-info/top_level.txt +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_agent_tools.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_apollo_company_search.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_apollo_lead_search.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_connectivity.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_email_body_utils.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_generate_email.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_generate_sms_whatsapp.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_google_document.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_hubspot_call_logs.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_linkedin_serper.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_mailreach.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_mcp_connectivity.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_normalize_graph_datetime.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_proxycurl_get_company_search_id.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_proxycurl_job_count.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_reply_thread_fallback.py +0 -0
- {dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/tests/test_send_email_recipients.py +0 -0
- {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.
|
|
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-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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
|
-
|
|
1136
|
-
|
|
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
|
-
|
|
2223
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_email_validity_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/check_linkedin_url_validity.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/compose_three_step_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/extract_email_content_for_llm.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/generate_linkedin_connect_message.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openai_assistant_and_file_utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/openapi_tool/openapi_tool.py
RENAMED
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/parse_linkedin_messages_txt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serarch_router_local_business.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dhisana-0.0.1.dev306 → dhisana-0.0.1.dev308}/src/dhisana/utils/serpapi_local_business_search.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|