Skip to content

Commit 02a9d8d

Browse files
committed
async http client for api fulfillment
1 parent e9725d7 commit 02a9d8d

File tree

6 files changed

+99
-63
lines changed

6 files changed

+99
-63
lines changed

app/admin/entities/store.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,13 @@ async def edit_entity(entity_id: str, entity_data: dict) -> dict:
2525
async def delete_entity(entity_id: str):
2626
await entity_collection.delete_one({"_id": ObjectId(entity_id)})
2727

28+
async def list_synonyms():
29+
""" list all synonyms across the entities"""
30+
synonyms = {}
31+
32+
entities = await list_entities()
33+
for entity in entities:
34+
for value in entity.entity_values:
35+
for synonym in value.synonyms:
36+
synonyms[synonym] = value.value
37+
return synonyms

app/bot/chat/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async def chat(request: Request, body: dict):
1414
try:
1515
# Access the dialogue manager from the fast api application state.
1616
chat_request = ChatModel.from_json(body)
17-
chat_response = request.app.state.dialogue_manager.process(chat_request)
17+
chat_response = await request.app.state.dialogue_manager.process(chat_request)
1818
return chat_response.to_json()
1919
except Exception as e:
2020
raise HTTPException(status_code=500, detail=f"Error processing request: {e}")

app/bot/dialogue_manager/dialogue_manager.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from app.bot.nlu.featurizers import SpacyFeaturizer
99
from app.bot.nlu.intent_classifiers import IntentClassifier
1010
from app.bot.nlu.entity_extractors import EntityExtractor
11-
from app.bot.dialogue_manager.utils import SilentUndefined, call_api, get_synonyms, split_sentence
11+
from app.bot.dialogue_manager.utils import SilentUndefined, split_sentence
12+
from app.admin.entities.store import list_synonyms
1213
from app.bot.dialogue_manager.models import ChatModel, IntentModel, ParameterModel
14+
from app.bot.dialogue_manager.http_client import call_api
1315
from app.config import app_config
1416

1517
logger = logging.getLogger('dialogue_manager')
@@ -32,7 +34,7 @@ async def from_config(cls):
3234
Initialize DialogueManager with all required dependencies
3335
"""
3436

35-
synonyms = await get_synonyms()
37+
synonyms = await list_synonyms()
3638

3739
# Initialize pipeline with components
3840
nlu_pipeline = NLUPipeline([
@@ -63,7 +65,7 @@ def update_model(self, models_dir):
6365
self.nlu_pipeline.load(models_dir)
6466
logger.info("NLU Pipeline models updated")
6567

66-
def process(self, chat_model: ChatModel) -> ChatModel:
68+
async def process(self, chat_model: ChatModel) -> ChatModel:
6769
"""
6870
Single entry point to process the dialogue request.
6971
@@ -110,7 +112,7 @@ def process(self, chat_model: ChatModel) -> ChatModel:
110112

111113
# Step 5: Handle API trigger if the intent is complete
112114
if chat_model_response.complete:
113-
chat_model_response = self._handle_api_trigger(active_intent, chat_model_response, context)
115+
chat_model_response = await self._handle_api_trigger(active_intent, chat_model_response, context)
114116

115117
logger.info(f"Processed input: {chat_model_response.input_text}", extra=chat_model_response.to_json())
116118
return chat_model_response
@@ -233,27 +235,27 @@ def _handle_missing_parameters(self, parameters: List[ParameterModel], chat_mode
233235

234236
return chat_model_response
235237

236-
def _handle_api_trigger(self, intent: IntentModel, chat_model_response: ChatModel, context: Dict) -> ChatModel:
238+
async def _handle_api_trigger(self, intent: IntentModel, chat_model_response: ChatModel, context: Dict) -> ChatModel:
237239
"""
238240
Handle API trigger if the intent requires it.
239241
"""
240242
if intent.api_trigger and intent.api_details:
241243
try:
242-
result = self._call_intent_api(intent, context)
244+
result = await self._call_intent_api(intent, context)
243245
context["result"] = result
244-
template = Template(intent.speech_response, undefined=SilentUndefined)
245-
chat_model_response.speech_response = split_sentence(template.render(**context))
246+
template = Template(intent.speech_response, undefined=SilentUndefined, enable_async=True)
247+
chat_model_response.speech_response = split_sentence(await template.render_async(**context))
246248
except Exception as e:
247249
logger.warning(f"API call failed: {e}")
248250
chat_model_response.speech_response = ["Service is not available. Please try again later."]
249251
else:
250252
context["result"] = {}
251-
template = Template(intent.speech_response, undefined=SilentUndefined)
252-
chat_model_response.speech_response = split_sentence(template.render(**context))
253+
template = Template(intent.speech_response, undefined=SilentUndefined, enable_async=True)
254+
chat_model_response.speech_response = split_sentence(await template.render_async(**context))
253255

254256
return chat_model_response
255257

256-
def _call_intent_api(self, intent: IntentModel, context: Dict):
258+
async def _call_intent_api(self, intent: IntentModel, context: Dict):
257259
"""
258260
Call the API associated with the intent.
259261
"""
@@ -268,4 +270,4 @@ def _call_intent_api(self, intent: IntentModel, context: Dict):
268270
else:
269271
parameters = context.get("parameters", {})
270272

271-
return call_api(rendered_url, api_details.request_type, headers, parameters, api_details.is_json)
273+
return await call_api(rendered_url, api_details.request_type, headers, parameters, api_details.is_json)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import json
2+
import logging
3+
import aiohttp
4+
import asyncio
5+
from typing import Dict, Any, Optional
6+
from aiohttp import ClientTimeout
7+
8+
logger = logging.getLogger("http_client")
9+
10+
async def call_api(
11+
url: str,
12+
method: str,
13+
headers: Optional[Dict[str, str]] = None,
14+
parameters: Optional[Dict[str, Any]] = None,
15+
is_json: bool = False,
16+
timeout: int = 30
17+
) -> Dict[str, Any]:
18+
"""
19+
Asynchronously call external API with improved error handling and timeout management
20+
21+
Args:
22+
url: The API endpoint URL
23+
method: HTTP method (GET, POST, PUT, DELETE)
24+
headers: Optional request headers
25+
parameters: Optional request parameters or body
26+
is_json: Whether to send parameters as JSON body
27+
timeout: Request timeout in seconds
28+
29+
Returns:
30+
Dict containing the API response
31+
32+
Raises:
33+
aiohttp.ClientError: For HTTP-specific errors
34+
asyncio.TimeoutError: When request times out
35+
ValueError: For invalid method types
36+
Exception: For other unexpected errors
37+
"""
38+
headers = headers or {}
39+
parameters = parameters or {}
40+
timeout_config = ClientTimeout(total=timeout)
41+
42+
try:
43+
async with aiohttp.ClientSession(timeout=timeout_config) as session:
44+
method = method.upper()
45+
logger.debug(f"Initiating async API Call: url={url} method={method} payload={parameters}")
46+
47+
if method == "GET":
48+
async with session.get(url, headers=headers, params=parameters) as response:
49+
result = await response.json()
50+
elif method in ["POST", "PUT"]:
51+
kwargs = {"headers": headers, "json" if is_json else "params": parameters}
52+
async with getattr(session, method.lower())(url, **kwargs) as response:
53+
result = await response.json()
54+
elif method == "DELETE":
55+
async with session.delete(url, headers=headers, params=parameters) as response:
56+
result = await response.json()
57+
else:
58+
raise ValueError(f"Unsupported request method: {method}")
59+
60+
response.raise_for_status()
61+
logger.debug(f"API response => {result}")
62+
return result
63+
64+
except aiohttp.ClientError as e:
65+
logger.error(f"HTTP error occurred: {str(e)}")
66+
raise
67+
except asyncio.TimeoutError:
68+
logger.error(f"Request timed out after {timeout} seconds")
69+
raise
70+
except Exception as e:
71+
logger.error(f"Unexpected error during API call: {str(e)}")
72+
raise

app/bot/dialogue_manager/utils.py

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,11 @@
1-
import json
21
import logging
3-
import requests
42
from jinja2 import Undefined
5-
from app.admin.entities.store import list_entities
63

74
logger = logging.getLogger("dialogue_manager")
85

96
def split_sentence(sentence):
107
return sentence.split("###")
118

12-
async def get_synonyms():
13-
"""
14-
Build synonyms dict from DB
15-
:return:
16-
"""
17-
synonyms = {}
18-
19-
entities = await list_entities()
20-
for entity in entities:
21-
for value in entity.entity_values:
22-
for synonym in value.synonyms:
23-
synonyms[synonym] = value.value
24-
return synonyms
25-
26-
def call_api(url, type, headers={}, parameters={}, is_json=False):
27-
"""
28-
Call external API
29-
:param url:
30-
:param type:
31-
:param parameters:
32-
:param is_json:
33-
:return:
34-
"""
35-
logger.debug("Initiating API Call with following info: url => {} payload => {}".format(url, parameters))
36-
if "GET" in type:
37-
response = requests.get(url, headers=headers, params=parameters, timeout=5)
38-
elif "POST" in type:
39-
if is_json:
40-
response = requests.post(url, headers=headers, json=parameters, timeout=5)
41-
else:
42-
response = requests.post(url, headers=headers, params=parameters, timeout=5)
43-
elif "PUT" in type:
44-
if is_json:
45-
response = requests.put(url, headers=headers, json=parameters, timeout=5)
46-
else:
47-
response = requests.put(url, headers=headers, params=parameters, timeout=5)
48-
elif "DELETE" in type:
49-
response = requests.delete(url, headers=headers, params=parameters, timeout=5)
50-
else:
51-
raise Exception("unsupported request method.")
52-
result = json.loads(response.text)
53-
logger.debug("API response => %s", result)
54-
return result
55-
56-
579
class SilentUndefined(Undefined):
5810
"""
5911
Class to suppress jinja2 errors and warnings

app/bot/nlu/training.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from app.bot.nlu.featurizers import SpacyFeaturizer
77
from app.bot.nlu.intent_classifiers import IntentClassifier
88
from app.bot.nlu.entity_extractors import EntityExtractor
9-
from app.bot.dialogue_manager.utils import get_synonyms
9+
from app.admin.entities.store import list_synonyms
1010

1111
from app.config import app_config
1212

@@ -36,7 +36,7 @@ async def train_pipeline(app):
3636
training_data.append(example)
3737

3838
# initialize and train pipeline
39-
synonyms = await get_synonyms()
39+
synonyms = await list_synonyms()
4040
pipeline = NLUPipeline([
4141
SpacyFeaturizer(spacy_model_name),
4242
IntentClassifier(),

0 commit comments

Comments
 (0)