Skip to content

Commit 335ec77

Browse files
authored
[QUO-1183][QUO-1186] Add date and uuid as client defined (#91)
* add date and uuid * Working id create * add polling * fix url * fix tests
1 parent 3e52915 commit 335ec77

File tree

6 files changed

+595
-172
lines changed

6 files changed

+595
-172
lines changed

examples/logging/async_simple_logging.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,23 @@
22
import asyncio
33

44
quotient = AsyncQuotientAI()
5-
65
quotient_logger = quotient.logger.init(
76
# Required
8-
app_name="my-app",
7+
app_name="test-id-create",
98
environment="dev",
109
# dynamic labels for slicing/dicing analytics e.g. by customer, feature, etc
1110
tags={"model": "gpt-4o", "feature": "customer-support"},
1211
hallucination_detection=True,
1312
inconsistency_detection=True,
14-
sample_rate=1.0,
13+
hallucination_detection_sample_rate=1.0,
1514
)
1615

1716

1817
async def main():
1918
# Mock retrieved documents
2019
retrieved_documents = [{"page_content": "Sample document"}]
2120

22-
await quotient_logger.log(
21+
log_id = await quotient_logger.log(
2322
user_query="Sample input",
2423
model_output="Sample output",
2524
# Page content from Documents from your retriever used to generate the model output
@@ -40,6 +39,38 @@ async def main():
4039
)
4140

4241
print("Log request sent")
42+
print("Log ID: ", log_id)
43+
print("Log created, waiting for detection results...")
44+
45+
# Poll for detection results with a timeout of 60 seconds
46+
# You can adjust timeout and poll_interval based on your needs
47+
detection_results = await quotient_logger.poll_for_detection(
48+
log_id=log_id,
49+
timeout=60, # Wait up to 60 seconds for results
50+
poll_interval=2.0, # Check every 2 seconds
51+
)
52+
53+
if detection_results:
54+
print("\nDetection Results:")
55+
print(f"Status: {detection_results.status}")
56+
print(f"Has hallucination: {detection_results.has_hallucination}")
57+
58+
if detection_results.has_hallucination is not None:
59+
print(f"Has hallucinations: {detection_results.has_hallucination}")
60+
61+
if detection_results.evaluations:
62+
print(f"\nFound {len(detection_results.evaluations)} evaluations")
63+
for i, eval in enumerate(detection_results.evaluations):
64+
print(f"\nEvaluation {i+1}:")
65+
print(f"Sentence: {eval.get('sentence', 'N/A')}")
66+
print(f"Is hallucinated: {eval.get('is_hallucinated', 'N/A')}")
67+
else:
68+
print(
69+
"\nNo detection results received. The detection might still be in progress or failed."
70+
)
71+
print("You can try again later with:")
72+
print(f"await quotient_logger.get_detection(log_id='{log_id}')")
73+
4374
print("Press Enter to exit...")
4475

4576
# Use asyncio's run_in_executor to handle blocking input() call

examples/logging/simple_logging.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
tags={"model": "gpt-4o", "feature": "customer-support"},
1010
hallucination_detection=True,
1111
inconsistency_detection=True,
12+
hallucination_detection_sample_rate=1.0,
1213
)
1314

1415
# Mock retrieved documents
1516
retrieved_documents = [{"page_content": "Sample document"}]
1617

17-
quotient_logger.log(
18+
log_id = quotient_logger.log(
1819
user_query="Sample input",
1920
model_output="Sample output",
2021
# Page content from Documents from your retriever used to generate the model output
@@ -34,4 +35,34 @@
3435
tags={"model": "gpt-4o-mini", "feature": "customer-support"},
3536
)
3637

37-
print("Log created")
38+
print("Log ID: ", log_id)
39+
print("Log created, waiting for detection results...")
40+
41+
# Poll for detection results with a timeout of 60 seconds
42+
# You can adjust timeout and poll_interval based on your needs
43+
detection_results = quotient_logger.poll_for_detection(
44+
log_id=log_id,
45+
timeout=60, # Wait up to 60 seconds for results
46+
poll_interval=2.0, # Check every 2 seconds
47+
)
48+
49+
if detection_results:
50+
print("\nDetection Results:")
51+
print(f"Status: {detection_results.status}")
52+
print(f"Has hallucination: {detection_results.has_hallucination}")
53+
54+
if detection_results.has_hallucination is not None:
55+
print(f"Has hallucinations: {detection_results.has_hallucination}")
56+
57+
if detection_results.evaluations:
58+
print(f"\nFound {len(detection_results.evaluations)} evaluations")
59+
for i, eval in enumerate(detection_results.evaluations):
60+
print(f"\nEvaluation {i+1}:")
61+
print(f"Sentence: {eval.get('sentence', 'N/A')}")
62+
print(f"Is hallucinated: {eval.get('is_hallucinated', 'N/A')}")
63+
else:
64+
print(
65+
"\nNo detection results received. The detection might still be in progress or failed."
66+
)
67+
print("You can try again later with:")
68+
print(f"quotient_logger.get_detection(log_id='{log_id}')")

quotientai/async_client.py

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ def __init__(self, api_key: str):
3232
self.token = None
3333
self.token_expiry = 0
3434
self.token_api_key = None
35-
self._token_path = token_dir / ".quotient" / f"{api_key[-6:]+'_' if api_key else ''}auth_token.json"
35+
self._token_path = (
36+
token_dir
37+
/ ".quotient"
38+
/ f"{api_key[-6:]+'_' if api_key else ''}auth_token.json"
39+
)
3640

3741
# Try to load existing token
3842
self._load_token()
@@ -56,11 +60,15 @@ def _save_token(self, token: str, expiry: int):
5660
try:
5761
self._token_path.parent.mkdir(parents=True, exist_ok=True)
5862
except Exception:
59-
logger.error(f"could not create directory for token. if you see this error please notify us at [email protected]")
63+
logger.error(
64+
f"could not create directory for token. if you see this error please notify us at [email protected]"
65+
)
6066
return None
6167
# Save to disk
6268
with open(self._token_path, "w") as f:
63-
json.dump({"token": token, "expires_at": expiry, "api_key": self.api_key}, f)
69+
json.dump(
70+
{"token": token, "expires_at": expiry, "api_key": self.api_key}, f
71+
)
6472

6573
def _load_token(self):
6674
"""Load token from disk if available"""
@@ -80,10 +88,10 @@ def _load_token(self):
8088
def _is_token_valid(self):
8189
"""Check if token exists and is not expired"""
8290
self._load_token()
83-
91+
8492
if not self.token:
8593
return False
86-
94+
8795
if self.token_api_key != self.api_key:
8896
return False
8997

@@ -210,7 +218,7 @@ def init(
210218
if not (0.0 <= self.sample_rate <= 1.0):
211219
logger.error(f"sample_rate must be between 0.0 and 1.0")
212220
return None
213-
221+
214222
self.hallucination_detection = hallucination_detection
215223
self.inconsistency_detection = inconsistency_detection
216224
self._configured = True
@@ -242,7 +250,9 @@ async def log(
242250
underlying non_blocking_create function.
243251
"""
244252
if not self._configured:
245-
logger.error(f"Logger is not configured. Please call init() before logging.")
253+
logger.error(
254+
f"Logger is not configured. Please call init() before logging."
255+
)
246256
return None
247257

248258
# Merge default tags with any tags provided at log time.
@@ -269,15 +279,19 @@ async def log(
269279
try:
270280
LogDocument(**doc)
271281
except Exception as e:
272-
logger.error(f"Invalid document format: Documents must include 'page_content' field and optional 'metadata' object with string keys.")
282+
logger.error(
283+
f"Invalid document format: Documents must include 'page_content' field and optional 'metadata' object with string keys."
284+
)
273285
return None
274286
else:
275287
actual_type = type(doc).__name__
276-
logger.error(f"Invalid document type: Received {actual_type}, but documents must be strings or dictionaries.")
288+
logger.error(
289+
f"Invalid document type: Received {actual_type}, but documents must be strings or dictionaries."
290+
)
277291
return None
278-
292+
279293
if self._should_sample():
280-
await self.logs_resource.create(
294+
log_id = await self.logs_resource.create(
281295
app_name=self.app_name,
282296
environment=self.environment,
283297
user_query=user_query,
@@ -290,9 +304,40 @@ async def log(
290304
inconsistency_detection=inconsistency_detection,
291305
hallucination_detection_sample_rate=self.hallucination_detection_sample_rate,
292306
)
293-
307+
return log_id
294308
return None
295309

310+
async def poll_for_detection(
311+
self, log_id: str, timeout: int = 300, poll_interval: float = 2.0
312+
):
313+
"""
314+
Get Detection results for a log asynchronously.
315+
316+
This method polls the Detection endpoint until the results are ready or the timeout is reached.
317+
318+
Args:
319+
log_id: The ID of the log to get Detection results for
320+
timeout: Maximum time to wait for results in seconds (default: 300s/5min)
321+
poll_interval: How often to poll the API in seconds (default: 2s)
322+
323+
Returns:
324+
Log object with Detection results if successful, None otherwise
325+
"""
326+
if not self._configured:
327+
logger.error(
328+
f"Logger is not configured. Please call init() before getting Detection results."
329+
)
330+
return None
331+
332+
if not log_id:
333+
logger.error("Log ID is required for Detection")
334+
return None
335+
336+
# Call the underlying resource method
337+
return await self.logs_resource.poll_for_detection(
338+
log_id, timeout, poll_interval
339+
)
340+
296341

297342
class AsyncQuotientAI:
298343
"""
@@ -310,9 +355,11 @@ class AsyncQuotientAI:
310355
def __init__(self, api_key: Optional[str] = None):
311356
self.api_key = api_key or os.environ.get("QUOTIENT_API_KEY")
312357
if not self.api_key:
313-
logger.error("could not find API key. either pass api_key to AsyncQuotientAI() or "
358+
logger.error(
359+
"could not find API key. either pass api_key to AsyncQuotientAI() or "
314360
"set the QUOTIENT_API_KEY environment variable. "
315-
f"if you do not have an API key, you can create one at https://app.quotientai.co in your settings page")
361+
f"if you do not have an API key, you can create one at https://app.quotientai.co in your settings page"
362+
)
316363

317364
self._client = _AsyncQuotientClient(self.api_key)
318365
self.auth = AsyncAuthResource(self._client)
@@ -331,7 +378,8 @@ def __init__(self, api_key: Optional[str] = None):
331378
except Exception as e:
332379
logger.error(
333380
"If you are seeing this error, please check that your API key is correct.\n"
334-
f"If the issue persists, please contact [email protected]\n{traceback.format_exc()}")
381+
f"If the issue persists, please contact [email protected]\n{traceback.format_exc()}"
382+
)
335383
return None
336384

337385
async def evaluate(
@@ -362,9 +410,11 @@ def _validate_parameters(parameters):
362410

363411
invalid_parameters = set(parameters.keys()) - set(valid_parameters)
364412
if invalid_parameters:
365-
logger.error(f"invalid parameters: {', '.join(invalid_parameters)}. \nvalid parameters are: {', '.join(valid_parameters)}")
413+
logger.error(
414+
f"invalid parameters: {', '.join(invalid_parameters)}. \nvalid parameters are: {', '.join(valid_parameters)}"
415+
)
366416
return None
367-
417+
368418
return parameters
369419

370420
v_parameters = _validate_parameters(parameters)

0 commit comments

Comments
 (0)