1+ """
2+ /utils/network/openai_client.py
3+
4+ This software is released under the AGPL-3.0 license
5+ Copyright (c) 2023-2024 Braedon Hendy
6+
7+ Further updates and packaging added in 2024 through the ClinicianFOCUS initiative,
8+ a collaboration with Dr. Braedon Hendy and Conestoga College Institute of Applied
9+ Learning and Technology as part of the CNERG+ applied research project,
10+ Unburdening Primary Healthcare: An Open-Source AI Clinician Partner Platform".
11+ Prof. Michael Yingbull (PI), Dr. Braedon Hendy (Partner),
12+ and Research Students - Software Developer Alex Simko, Pemba Sherpa (F24), and Naitik Patel.
13+ """
14+
15+
116import asyncio
217import json
318import httpx
1025
1126
1227class OpenAIClient (BaseNetworkClient ):
13- """Client for communicating with OpenAI API."""
28+ """
29+ Client for communicating with OpenAI API endpoints.
30+
31+ This class provides an asynchronous interface for interacting with OpenAI's
32+ Chat Completion API and legacy Completion API. It supports cancellable requests,
33+ error handling, and configurable model parameters.
34+
35+ Features:
36+ - Asynchronous API calls with httpx
37+ - Request cancellation via threading and asyncio events
38+ - Automatic error handling and user-friendly error messages
39+ - Support for both Chat API (gpt-4, gpt-3.5-turbo) and Completion API
40+ - Configurable model parameters (temperature, max_tokens, etc.)
41+ - Thread-safe cancellation monitoring
42+
43+ Attributes:
44+ cancel_monitor_thread (threading.Thread): Thread for monitoring cancellation events
45+ monitoring_stop_event (threading.Event): Event to stop the monitoring thread
46+ stop_event (asyncio.Event): Async event for request cancellation
47+ threading_cancel_event (threading.Event): Threading event for cancellation signals
48+ checking_active (bool): Flag indicating if cancellation monitoring is active
49+
50+ Example:
51+ ```python
52+ config = NetworkConfig(host="https://api.openai.com/v1", api_key="your-key")
53+ client = OpenAIClient(config)
54+
55+ # Synchronous call
56+ response = client.send_chat_completion_sync(
57+ text="Hello, world!",
58+ model="gpt-4",
59+ threading_cancel_event=cancel_event
60+ )
61+
62+ # Asynchronous call
63+ response = await client.send_chat_completion(
64+ text="Hello, world!",
65+ model="gpt-4",
66+ stop_event=stop_event,
67+ threading_cancel_event=cancel_event
68+ )
69+ ```
70+ """
1471
1572 def __init__ (self , config : NetworkConfig ):
1673 super ().__init__ (config )
@@ -168,6 +225,13 @@ async def send_completion(
168225 await self ._close_client ()
169226
170227 def _apply_options (self , payload : Dict [str , Any ], options : Dict [str , Any ]) -> None :
228+ """
229+ Apply model configuration options to the API request payload.
230+
231+ Args:
232+ payload (Dict[str, Any]): The request payload to modify
233+ options (Dict[str, Any]): Dictionary of options to apply (temperature, max_tokens, etc.)
234+ """
171235 # Set default values
172236 payload .update ({
173237 "temperature" : 0.7 ,
@@ -201,7 +265,18 @@ def _build_payload(
201265 system_message : Optional [str ] = None ,
202266 ** options
203267 ) -> Dict [str , Any ]:
204- """Build the request payload for OpenAI Chat API."""
268+ """
269+ Build the request payload for OpenAI Chat API.
270+
271+ Args:
272+ text (str): The user message text to send
273+ model (str): The model name to use (e.g., 'gpt-4', 'gpt-3.5-turbo')
274+ system_message (Optional[str]): Optional system message to set context
275+ **options: Additional model options
276+
277+ Returns:
278+ Dict[str, Any]: The formatted request payload for the Chat API
279+ """
205280 messages = []
206281
207282 # Add system message if provided
@@ -222,7 +297,17 @@ def _build_payload(
222297 return payload
223298
224299 def _build_completion_payload (self , prompt : str , model : str , ** options ) -> Dict [str , Any ]:
225- """Build the request payload for OpenAI Completion API."""
300+ """
301+ Build the request payload for OpenAI Completion API.
302+
303+ Args:
304+ prompt (str): The prompt text to send to the API
305+ model (str): The model name to use
306+ **options: Additional model options
307+
308+ Returns:
309+ Dict[str, Any]: The formatted request payload for the Completion API
310+ """
226311 payload = {
227312 "model" : model .strip (),
228313 "prompt" : prompt
@@ -233,7 +318,19 @@ def _build_completion_payload(self, prompt: str, model: str, **options) -> Dict[
233318 return payload
234319
235320 async def _send (self , endpoint : str , payload : Dict [str , Any ]) -> Dict [str , Any ]:
236- """Unified method to send HTTP requests to OpenAI API endpoints."""
321+ """
322+ Unified method to send HTTP requests to OpenAI API endpoints.
323+
324+ Args:
325+ endpoint (str): The API endpoint path (e.g., 'chat/completions')
326+ payload (Dict[str, Any]): The request payload to send
327+
328+ Returns:
329+ Dict[str, Any]: The JSON response from the API
330+
331+ Raises:
332+ httpx.HTTPStatusError: If the API returns an error status code
333+ """
237334 url = f"{ self .config .host } /{ endpoint } "
238335 headers = {
239336 "Authorization" : f"Bearer { self .config .api_key } " ,
@@ -244,23 +341,47 @@ async def _send(self, endpoint: str, payload: Dict[str, Any]) -> Dict[str, Any]:
244341 return response .json ()
245342
246343 def _parse_response_data (self , response_data : Dict [str , Any ]) -> str :
247- """Parse the response data from OpenAI Chat API."""
344+ """
345+ Parse the response data from OpenAI Chat API.
346+
347+ Args:
348+ response_data (Dict[str, Any]): The raw response data from the API
349+
350+ Returns:
351+ str: The extracted message content or error message if parsing fails
352+ """
248353 try :
249354 return response_data ['choices' ][0 ]['message' ]['content' ]
250355 except (KeyError , IndexError ) as e :
251356 logger .error (f"Error parsing OpenAI response structure: { e } " )
252357 return 'Error: Invalid response format from OpenAI API'
253358
254359 def _parse_completion_response_data (self , response_data : Dict [str , Any ]) -> str :
255- """Parse the response data from OpenAI Completion API."""
360+ """
361+ Parse the response data from OpenAI Completion API.
362+
363+ Args:
364+ response_data (Dict[str, Any]): The raw response data from the API
365+
366+ Returns:
367+ str: The extracted completion text or error message if parsing fails
368+ """
256369 try :
257370 return response_data ['choices' ][0 ]['text' ].strip ()
258371 except (KeyError , IndexError ) as e :
259372 logger .error (f"Error parsing OpenAI completion response structure: { e } " )
260373 return 'Error: Invalid response format from OpenAI Completion API'
261374
262375 def _handle_error (self , error : Exception ) -> str :
263- """Handle and categorize different types of errors specific to OpenAI."""
376+ """
377+ Handle and categorize different types of errors specific to OpenAI.
378+
379+ Args:
380+ error (Exception): The exception that occurred during the API request
381+
382+ Returns:
383+ str: A user-friendly error message describing the issue
384+ """
264385 if isinstance (error , httpx .ReadError ):
265386 return "Network connection lost to OpenAI API. Please check your internet connection."
266387 elif isinstance (error , httpx .ConnectError ):
@@ -283,7 +404,12 @@ def _handle_error(self, error: Exception) -> str:
283404 return f"OpenAI API request failed: { str (error )} "
284405
285406 async def cancel_request (self ):
286- """Cancel an ongoing API request by setting the stop event and closing the client."""
407+ """
408+ Cancel an ongoing API request by setting the stop event and closing the client.
409+
410+ This method gracefully closes the HTTP client connection to terminate
411+ any pending requests.
412+ """
287413 try :
288414 logger .info ("Cancelling OpenAI API request..." )
289415 await self ._close_client ()
@@ -292,7 +418,12 @@ async def cancel_request(self):
292418 logger .exception (f"Error during OpenAI API cancellation: { e } " )
293419
294420 def start_cancel_monitoring (self ):
295- """Start the cancellation monitoring loop in a separate thread."""
421+ """
422+ Start the cancellation monitoring loop in a separate thread.
423+
424+ Begins monitoring the threading cancel event to detect cancellation
425+ requests and handle them appropriately by closing the client connection.
426+ """
296427 if self .threading_cancel_event :
297428 self .checking_active = True
298429 self .monitoring_stop_event .clear ()
@@ -304,7 +435,13 @@ def start_cancel_monitoring(self):
304435 self .cancel_monitor_thread .start ()
305436
306437 def _monitor_cancellation (self ):
307- """Monitor cancellation event in a separate thread."""
438+ """
439+ Monitor cancellation event in a separate thread.
440+
441+ Continuously checks for cancellation signals and closes the client
442+ connection when cancellation is detected. Runs in a daemon thread
443+ with periodic sleep intervals.
444+ """
308445 while self .checking_active and not self .monitoring_stop_event .is_set ():
309446 try :
310447 if self .threading_cancel_event is None :
@@ -339,7 +476,12 @@ def close_client():
339476 break
340477
341478 def stop_cancel_monitoring (self ):
342- """Stop the cancellation monitoring."""
479+ """
480+ Stop the cancellation monitoring.
481+
482+ Terminates the cancellation monitoring thread and cleans up
483+ associated events and resources.
484+ """
343485 logger .info ("Stopping OpenAI API cancellation monitoring." )
344486 self .checking_active = False
345487 self .monitoring_stop_event .set ()
0 commit comments