Skip to content

Commit 805bdb4

Browse files
authored
[QUO-869] Update logger to be non blocking (#73)
* update to be non blocking * restore url * pr feedback * remove logs * Remove thread pool exec * name * dequeue
1 parent 0d5e194 commit 805bdb4

File tree

9 files changed

+239
-78
lines changed

9 files changed

+239
-78
lines changed

examples/logging/async_simple_logging.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@
1111
tags={"model": "gpt-4o", "feature": "customer-support"},
1212
hallucination_detection=True,
1313
inconsistency_detection=True,
14+
sample_rate=1.0,
1415
)
1516

1617

1718
async def main():
1819
# Mock retrieved documents
1920
retrieved_documents = [{"page_content": "Sample document"}]
2021

21-
response = await quotient_logger.log(
22+
await quotient_logger.log(
2223
user_query="Sample input",
2324
model_output="Sample output",
2425
# Page content from Documents from your retriever used to generate the model output
@@ -38,7 +39,14 @@ async def main():
3839
tags={"model": "gpt-4o-mini", "feature": "customer-support"},
3940
)
4041

41-
print(response)
42+
print("Log request sent")
43+
print("Press Enter to exit...")
44+
45+
# Use asyncio's run_in_executor to handle blocking input() call
46+
loop = asyncio.get_running_loop()
47+
await loop.run_in_executor(None, input)
48+
49+
print("Exiting...")
4250

4351

4452
# Run the async function
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# FastAPI Example with Quotient Logging
2+
3+
This example demonstrates how to use Quotient's logging capabilities with a FastAPI application.
4+
5+
## Installation
6+
7+
Install the Quotient Python package as an editable pip package:
8+
9+
```bash
10+
pip install -e ../../../../quotient-python
11+
```
12+
13+
## Running the Application
14+
15+
Start the FastAPI application with Uvicorn:
16+
17+
```bash
18+
uvicorn main:app --reload
19+
```
20+
21+
This will start the server with hot-reloading enabled for development.

examples/logging/example_fast_api/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@
1313
### Instructions
1414
{{instructions}}
1515
"""
16+
# Documents that may contain context that model output can be attributed to
1617
RETRIEVED_DOCUMENTS = [
1718
{
1819
"page_content": "Our company has unlimited vacation days",
1920
"metadata": {"document_id": "123"},
2021
}
2122
]
2223
QUESTION = "What is the company's vacation policy?"
24+
# System instructions that may contain context that model output can be attributed to
2325
INSTRUCTIONS = ["If you do not know the answer, just say that you do not know."]
26+
# Historical messages that may contain context that model output can be attributed to
27+
MESSAGE_HISTORY = [
28+
{"role": "user", "content": "What is the company's vacation policy?"},
29+
{"role": "assistant", "content": "Our company has unlimited vacation days"},
30+
]

examples/logging/example_fast_api/log.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
from openai import OpenAI, AsyncOpenAI
66
from quotientai import QuotientAI, AsyncQuotientAI
77

8-
from constants import INSTRUCTIONS, RETRIEVED_DOCUMENTS, QUESTION, PROMPT, INSTRUCTIONS
8+
from constants import (
9+
INSTRUCTIONS,
10+
RETRIEVED_DOCUMENTS,
11+
QUESTION,
12+
PROMPT,
13+
MESSAGE_HISTORY,
14+
)
915

1016
# Load environment variables
1117
load_dotenv()
@@ -19,23 +25,27 @@
1925
########################################################
2026
# Initialize QuotientAI and QuotientAI Logger
2127
########################################################
22-
quotient = QuotientAI()
28+
quotient = QuotientAI(api_key=os.environ.get("QUOTIENT_API_KEY"))
2329
quotient_logger = quotient.logger.init(
2430
app_name="my-app",
2531
environment="dev",
2632
tags={"model": "gpt-4o", "feature": "customer-support"},
2733
hallucination_detection=True,
34+
hallucination_detection_sample_rate=0.0,
35+
sample_rate=1.0,
2836
)
2937

3038
########################################################
3139
# Initialize Async QuotientAI Logger
3240
########################################################
33-
async_quotient = AsyncQuotientAI()
41+
async_quotient = AsyncQuotientAI(api_key=os.environ.get("QUOTIENT_API_KEY"))
3442
quotient_async_logger = async_quotient.logger.init(
3543
app_name="my-app",
3644
environment="dev",
3745
tags={"model": "gpt-4o", "feature": "customer-support"},
3846
hallucination_detection=True,
47+
hallucination_detection_sample_rate=0.0,
48+
sample_rate=1.0,
3949
)
4050

4151

@@ -45,7 +55,12 @@ def create_log():
4555
Create a log for the model completion using BackgroundTasks to create the log in the background
4656
"""
4757
formatted_prompt = chevron.render(
48-
PROMPT, {"context": RETRIEVED_DOCUMENTS, "question": QUESTION, "instructions": INSTRUCTIONS}
58+
PROMPT,
59+
{
60+
"context": RETRIEVED_DOCUMENTS,
61+
"question": QUESTION,
62+
"instructions": INSTRUCTIONS,
63+
},
4964
)
5065

5166
response = client.chat.completions.create(
@@ -70,6 +85,7 @@ def create_log():
7085
model_output=model_output,
7186
documents=document_contents,
7287
instructions=INSTRUCTIONS,
88+
message_history=MESSAGE_HISTORY,
7389
)
7490

7591
return {"response": model_output}
@@ -78,7 +94,12 @@ def create_log():
7894
@router.post("/create-log-async/")
7995
async def create_log_async():
8096
formatted_prompt = chevron.render(
81-
PROMPT, {"context": RETRIEVED_DOCUMENTS, "question": QUESTION, "instructions": INSTRUCTIONS}
97+
PROMPT,
98+
{
99+
"context": RETRIEVED_DOCUMENTS,
100+
"question": QUESTION,
101+
"instructions": INSTRUCTIONS,
102+
},
82103
)
83104

84105
response = await async_client.chat.completions.create(
@@ -103,6 +124,7 @@ async def create_log_async():
103124
model_output=model_output,
104125
documents=document_contents,
105126
instructions=INSTRUCTIONS,
127+
message_history=MESSAGE_HISTORY,
106128
)
107129

108130
return {"response": model_output}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
from fastapi import FastAPI
22
from log import router as log_router
3+
import logging
4+
5+
# Configure logging
6+
logging.basicConfig(
7+
level=logging.INFO,
8+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
9+
handlers=[logging.StreamHandler()]
10+
)
311

412
app = FastAPI()
513

14+
app.title = "QuotientAI Logger Testing"
15+
app.description = "A simple API for testing logging and detection with QuotientAI"
16+
617
app.include_router(log_router)

examples/logging/simple_logging.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# Mock retrieved documents
1515
retrieved_documents = [{"page_content": "Sample document"}]
1616

17-
response = quotient_logger.log(
17+
quotient_logger.log(
1818
user_query="Sample input",
1919
model_output="Sample output",
2020
# Page content from Documents from your retriever used to generate the model output
@@ -34,4 +34,4 @@
3434
tags={"model": "gpt-4o-mini", "feature": "customer-support"},
3535
)
3636

37-
print(response)
37+
print("Log created")

quotientai/async_client.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import random
23
from typing import Any, Dict, List, Optional
34

45
import httpx
@@ -19,10 +20,12 @@ def __init__(self, api_key: str):
1920
)
2021

2122
@handle_async_errors
22-
async def _get(self, path: str, params: Optional[Dict[str, Any]] = None, timeout: int = None) -> dict:
23+
async def _get(
24+
self, path: str, params: Optional[Dict[str, Any]] = None, timeout: int = None
25+
) -> dict:
2326
"""
2427
Send an async GET request to the specified path.
25-
28+
2629
Args:
2730
path: API endpoint path
2831
params: Optional query parameters
@@ -73,6 +76,7 @@ def __init__(self, logs_resource):
7376
self.app_name: Optional[str] = None
7477
self.environment: Optional[str] = None
7578
self.tags: Dict[str, Any] = {}
79+
self.sample_rate: float = 1.0
7680
self.hallucination_detection: bool = False
7781
self.inconsistency_detection: bool = False
7882
self._configured = False
@@ -84,6 +88,7 @@ def init(
8488
app_name: str,
8589
environment: str,
8690
tags: Optional[Dict[str, Any]] = {},
91+
sample_rate: float = 1.0,
8792
hallucination_detection: bool = False,
8893
inconsistency_detection: bool = False,
8994
hallucination_detection_sample_rate: float = 0,
@@ -95,12 +100,23 @@ def init(
95100
self.app_name = app_name
96101
self.environment = environment
97102
self.tags = tags or {}
103+
self.sample_rate = sample_rate
104+
105+
if not (0.0 <= self.sample_rate <= 1.0):
106+
raise QuotientAIError("sample_rate must be between 0.0 and 1.0")
107+
98108
self.hallucination_detection = hallucination_detection
99109
self.inconsistency_detection = inconsistency_detection
100110
self._configured = True
101111
self.hallucination_detection_sample_rate = hallucination_detection_sample_rate
102112
return self
103113

114+
def _should_sample(self) -> bool:
115+
"""
116+
Determine if the log should be sampled based on the sample rate.
117+
"""
118+
return random.random() < self.sample_rate
119+
104120
async def log(
105121
self,
106122
*,
@@ -139,21 +155,22 @@ async def log(
139155
else self.inconsistency_detection
140156
)
141157

142-
log = await self.logs_resource.create(
143-
app_name=self.app_name,
144-
environment=self.environment,
145-
user_query=user_query,
146-
model_output=model_output,
147-
documents=documents,
148-
message_history=message_history,
149-
instructions=instructions,
150-
tags=merged_tags,
151-
hallucination_detection=hallucination_detection,
152-
inconsistency_detection=inconsistency_detection,
153-
hallucination_detection_sample_rate=self.hallucination_detection_sample_rate,
154-
)
158+
if self._should_sample():
159+
await self.logs_resource.create(
160+
app_name=self.app_name,
161+
environment=self.environment,
162+
user_query=user_query,
163+
model_output=model_output,
164+
documents=documents,
165+
message_history=message_history,
166+
instructions=instructions,
167+
tags=merged_tags,
168+
hallucination_detection=hallucination_detection,
169+
inconsistency_detection=inconsistency_detection,
170+
hallucination_detection_sample_rate=self.hallucination_detection_sample_rate,
171+
)
155172

156-
return log
173+
return None
157174

158175

159176
class AsyncQuotientAI:

quotientai/client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ def __init__(self, api_key: str):
2020
)
2121

2222
@handle_errors
23-
def _get(self, path: str, params: Optional[Dict[str, Any]] = None, timeout: int = None) -> dict:
23+
def _get(
24+
self, path: str, params: Optional[Dict[str, Any]] = None, timeout: int = None
25+
) -> dict:
2426
"""
2527
Send a GET request to the specified path.
2628
@@ -154,7 +156,7 @@ def log(
154156
)
155157

156158
if self._should_sample():
157-
log = self.logs_resource.create(
159+
self.logs_resource.create(
158160
app_name=self.app_name,
159161
environment=self.environment,
160162
user_query=user_query,
@@ -168,7 +170,7 @@ def log(
168170
hallucination_detection_sample_rate=self.hallucination_detection_sample_rate,
169171
)
170172

171-
return log
173+
return None
172174
else:
173175
return None
174176

0 commit comments

Comments
 (0)