Skip to content

Commit a97c3ff

Browse files
authored
[QUO-758] Add Logs, Add Decorator, Add Hallucination Analysis (#65)
* Add Logs Resource, Add Decorator * styling, fixes * add boolean for hallucination * readme format * cleanup example api * remove background task * Remove decorator * cleanup unused * rename * PR Feedback * version * cleanup * name * endpoints and api * add rules * Feedback
1 parent 77c7643 commit a97c3ff

File tree

11 files changed

+336
-496
lines changed

11 files changed

+336
-496
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ celerybeat.pid
123123
.env
124124
.env.local
125125
.venv
126+
.venv.*
126127
env/
127128
venv/
128129
ENV/

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,32 @@ new_dataset = quotient.datasets.create(
5151

5252
print(new_dataset)
5353
```
54+
55+
**Create a log with hallucination detection:**
56+
Log an event with hallucination detection. This will create a log event in Quotient and perform hallucination detection on the model output, input, and documents. This is a fire and forget operation, so it will not block the execution of your code.
57+
58+
Additional examples can be found in the [examples](examples) directory.
59+
60+
```python
61+
from quotientai import QuotientAI
62+
63+
quotient = QuotientAI()
64+
quotient_logger = quotient.logger.init(
65+
# Required
66+
app_name="my-app",
67+
environment="dev",
68+
# dynamic labels for slicing/dicing analytics e.g. by customer, feature, etc
69+
tags={"model": "gpt-4o", "feature": "customer-support"},
70+
hallucination_detection=True,
71+
inconsistency_detection=True,
72+
)
73+
74+
quotient_logger.log(
75+
model_input="Sample input",
76+
model_output="Sample output",
77+
# Documents from your retriever used to generate the model output
78+
documents=[{"page_content": "Sample document"}],
79+
# optional additional context to help with hallucination detection, e.g. rules, constraints, etc
80+
contexts=["Sample context"],
81+
)
82+
```

examples/logging/example_fast_api/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
########################################################
2+
# Fixed constants for demonstration
3+
########################################################
4+
PROMPT = """
5+
You are a helpful assistant that can answer questions about the context. Follow the rules provided if they are relevant.
6+
7+
### Question
8+
{{question}}
9+
10+
### Context
11+
{{context}}
12+
13+
### Rules
14+
{{rules}}
15+
"""
16+
RETRIEVED_DOCUMENTS = [
17+
{
18+
"page_content": "Our company has unlimited vacation days",
19+
"metadata": {"document_id": "123"},
20+
}
21+
]
22+
QUESTION = "What is the company's vacation policy?"
23+
RULES = ["If you do not know the answer, just say that you do not know."]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import os
2+
import chevron
3+
from fastapi import APIRouter
4+
from dotenv import load_dotenv
5+
from openai import OpenAI
6+
from quotientai import QuotientAI
7+
from constants import RETRIEVED_DOCUMENTS, QUESTION, PROMPT, RULES
8+
9+
# Load environment variables
10+
load_dotenv()
11+
12+
# Initialize OpenAI client
13+
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
14+
15+
########################################################
16+
# Initialize QuotientAI and QuotientAI Logger
17+
########################################################
18+
quotient = QuotientAI()
19+
quotient_logger = quotient.logger.init(
20+
app_name="my-app",
21+
environment="dev",
22+
tags={"model": "gpt-4o", "feature": "customer-support"},
23+
hallucination_detection=True,
24+
)
25+
26+
# Create a router for the endpoint
27+
router = APIRouter()
28+
29+
30+
@router.post("/create-log/")
31+
async def create_log():
32+
"""
33+
Create a log for the model completion using BackgroundTasks to create the log in the background
34+
"""
35+
formatted_prompt = chevron.render(
36+
PROMPT, {"context": RETRIEVED_DOCUMENTS, "question": QUESTION, "rules": RULES}
37+
)
38+
39+
response = client.chat.completions.create(
40+
messages=[
41+
{
42+
"role": "user",
43+
"content": formatted_prompt,
44+
}
45+
],
46+
model="gpt-4o",
47+
)
48+
49+
model_output = response.choices[0].message.content
50+
51+
########################################################
52+
# Example implementation of creating a non-blocking log event
53+
########################################################
54+
quotient_logger.log(
55+
model_input=QUESTION,
56+
model_output=model_output,
57+
documents=RETRIEVED_DOCUMENTS,
58+
contexts=RULES,
59+
)
60+
61+
return {"response": model_output}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from fastapi import FastAPI
2+
from log import router as log_router
3+
4+
app = FastAPI()
5+
6+
app.include_router(log_router)

poetry.lock

Lines changed: 39 additions & 492 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
[tool.poetry]
22
name = "quotientai"
3-
version = "0.1.3"
3+
version = "0.1.4"
44
authors = [
55
"Freddie Vargus <[email protected]>",
6+
"Michael Goitia Sarmiento <[email protected]>",
67
]
78
description = "CLI for evaluating large language models with Quotient"
89
readme = "README.md"

quotientai/client.py

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

44
import httpx
55

@@ -11,7 +11,6 @@
1111
from quotientai.resources.runs import Run
1212

1313

14-
1514
class _BaseQuotientClient(httpx.Client):
1615
def __init__(self, api_key: str):
1716
super().__init__(
@@ -25,7 +24,7 @@ def _get(self, path: str) -> dict:
2524
return response
2625

2726
@handle_errors
28-
def _post(self, path: str, data: dict = {}) -> dict:
27+
def _post(self, path: str, data: dict = {}, timeout: int = None) -> dict:
2928
if isinstance(data, dict):
3029
data = {k: v for k, v in data.items() if v is not None}
3130
elif isinstance(data, list):
@@ -34,6 +33,7 @@ def _post(self, path: str, data: dict = {}) -> dict:
3433
response = self.post(
3534
url=path,
3635
json=data,
36+
timeout=timeout,
3737
)
3838
return response
3939

@@ -52,6 +52,93 @@ def _delete(self, path: str) -> dict:
5252
return response
5353

5454

55+
class QuotientLogger:
56+
"""
57+
Logger interface that wraps the underlying logs resource.
58+
This class handles both configuration (via init) and logging.
59+
"""
60+
61+
def __init__(self, logs_resource):
62+
self.logs_resource = logs_resource
63+
64+
self.app_name: Optional[str] = None
65+
self.environment: Optional[str] = None
66+
self.tags: Dict[str, Any] = {}
67+
self.hallucination_detection: bool = False
68+
self.inconsistency_detection: bool = False
69+
self._configured = False
70+
71+
def init(
72+
self,
73+
*,
74+
app_name: str,
75+
environment: str,
76+
tags: Optional[Dict[str, Any]] = {},
77+
hallucination_detection: bool = False,
78+
inconsistency_detection: bool = False,
79+
) -> "QuotientLogger":
80+
"""
81+
Configure the logger with the provided parameters and return self.
82+
This method must be called before using log().
83+
"""
84+
self.app_name = app_name
85+
self.environment = environment
86+
self.tags = tags or {}
87+
self.hallucination_detection = hallucination_detection
88+
self.inconsistency_detection = inconsistency_detection
89+
self._configured = True
90+
return self
91+
92+
def log(
93+
self,
94+
*,
95+
model_input: str,
96+
model_output: str,
97+
documents: List[dict],
98+
contexts: Optional[List[str]] = None,
99+
tags: Optional[Dict[str, Any]] = {},
100+
hallucination_detection: Optional[bool] = None,
101+
inconsistency_detection: Optional[bool] = None,
102+
):
103+
"""
104+
Log the model interaction asynchronously.
105+
106+
Merges the default tags (set via init) with any runtime-supplied tags and calls the
107+
underlying non_blocking_create function.
108+
"""
109+
if not self._configured:
110+
raise RuntimeError(
111+
"Logger is not configured. Please call init() before logging."
112+
)
113+
114+
# Merge default tags with any tags provided at log time.
115+
merged_tags = {**self.tags, **(tags or {})}
116+
117+
# Use the instance variable as the default if not provided
118+
hallucination_detection = (
119+
hallucination_detection
120+
if hallucination_detection is not None
121+
else self.hallucination_detection
122+
)
123+
inconsistency_detection = (
124+
inconsistency_detection
125+
if inconsistency_detection is not None
126+
else self.inconsistency_detection
127+
)
128+
129+
return self.logs_resource.non_blocking_create(
130+
app_name=self.app_name,
131+
environment=self.environment,
132+
model_input=model_input,
133+
model_output=model_output,
134+
documents=documents,
135+
contexts=contexts,
136+
tags=merged_tags,
137+
hallucination_detection=hallucination_detection,
138+
inconsistency_detection=inconsistency_detection,
139+
)
140+
141+
55142
class QuotientAI:
56143
"""
57144
A client that provides access to the QuotientAI API.
@@ -77,7 +164,10 @@ def __init__(self):
77164
self.models = resources.ModelsResource(_client)
78165
self.runs = resources.RunsResource(_client)
79166
self.metrics = resources.MetricsResource(_client)
167+
self.logs = resources.LogsResource(_client)
80168

169+
# Create an unconfigured logger instance.
170+
self.logger = QuotientLogger(self.logs)
81171

82172
def evaluate(
83173
self,

quotientai/resources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from quotientai.resources.datasets import DatasetsResource
44
from quotientai.resources.runs import RunsResource
55
from quotientai.resources.metrics import MetricsResource
6+
from quotientai.resources.logs import LogsResource
67

78
__all__ = [
89
"PromptsResource",
910
"ModelsResource",
1011
"DatasetsResource",
1112
"RunsResource",
1213
"MetricsResource",
14+
"LogsResource",
1315
]

0 commit comments

Comments
 (0)