Skip to content

Commit 642c883

Browse files
pjoshi30Preetam Joshi
andauthored
[Higher Level API] Added decorators for better dev UX and a Chatbot application (#19)
Adding the Aimon Rely README, images, the postman collection, a simple client and examples. A few small changes for error handling in the client and the example application. Getting the Aimon API key from the streamlit app updating README Updating langchain example gif Updating API endpoint Adding V2 API with support for conciseness, completeness and toxicity checks (#1) * Adding V2 API with support for conciseness, completeness and toxicity checks. * Removing prints and updating config for the example application. * Updating README --------- Co-authored-by: Preetam Joshi <info@aimon.ai> Updating postman collection Fixed the simple aimon client's handling of batch requests. Updated postman collection. Added support for a user_query parameter in the input data dictionary. Updating readme Fixed bug in the example app Uploading client code Adding more convenience APIs Fixing bug in create_dataset Added Github actions config to publish to PyPI. Cleaned up dependencies and updated documentation. Fixing langchain example Fixing doc links Formatting changes Changes for aimon-rely * Adding instruction adherence and hallucination v0.2 to the client Updating git ignore Adding more to gitignore Removing .idea files * Fixing doc string * Updating documentation * Updating Client to use V3 API * Fixing test * Updating tests * Updating documentation in the client * Adding .streamlit dir to .gitignore * initial version of decorators for syntactic sugar * A few more changes * updating analyze and detect decorators * Adding new notebooks * Fixing bug in analyze decorator --------- Co-authored-by: Preetam Joshi <info@aimon.ai>
1 parent b4b4de3 commit 642c883

11 files changed

+658
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ lib64
2222

2323
# Installer logs
2424
pip-log.txt
25+
26+
.streamlit/*

aimon/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"DEFAULT_MAX_RETRIES",
6363
"DEFAULT_CONNECTION_LIMITS",
6464
"DefaultHttpxClient",
65-
"DefaultAsyncHttpxClient",
65+
"DefaultAsyncHttpxClient"
6666
]
6767

6868
_setup_logging()
@@ -79,3 +79,6 @@
7979
except (TypeError, AttributeError):
8080
# Some of our exported symbols are builtins which we can't set attributes for.
8181
pass
82+
83+
from .decorators.detect import DetectWithContextQuery, DetectWithContextQueryInstructions, DetectWithQueryFuncReturningContext, DetectWithQueryInstructionsFuncReturningContext
84+
from .decorators.analyze import Analyze, Application, Model

aimon/decorators/__init__.py

Whitespace-only changes.

aimon/decorators/analyze.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from functools import wraps
2+
from .common import AimonClientSingleton
3+
4+
5+
class Application:
6+
def __init__(self, name, stage="evaluation", type="text", metadata={}):
7+
self.name = name
8+
self.stage = stage
9+
self.type = type
10+
self.metadata = metadata
11+
12+
13+
class Model:
14+
def __init__(self, name, model_type, metadata={}):
15+
self.name = name
16+
self.model_type = model_type
17+
self.metadata = metadata
18+
19+
20+
class Analyze(object):
21+
DEFAULT_CONFIG = {'hallucination': {'detector_name': 'default'}}
22+
23+
def __init__(self, application, model, api_key=None, evaluation_name=None, dataset_collection_name=None, eval_tags=None, instructions=None, config=None):
24+
self.client = AimonClientSingleton.get_instance(api_key)
25+
self.application = application
26+
self.model = model
27+
self.evaluation_name = evaluation_name
28+
self.dataset_collection_name = dataset_collection_name
29+
self.eval_tags = eval_tags
30+
self.instructions = instructions
31+
self.config = config if config else self.DEFAULT_CONFIG
32+
self.initialize()
33+
34+
def initialize(self):
35+
# Create or retrieve the model
36+
self._am_model = self.client.models.create(
37+
name=self.model.name,
38+
type=self.model.model_type,
39+
description="This model is named {} and is of type {}".format(self.model.name, self.model.model_type),
40+
metadata=self.model.metadata
41+
)
42+
43+
# Create or retrieve the application
44+
self._am_app = self.client.applications.create(
45+
name=self.application.name,
46+
model_name=self._am_model.name,
47+
stage=self.application.stage,
48+
type=self.application.type,
49+
metadata=self.application.metadata
50+
)
51+
52+
if self.evaluation_name is not None:
53+
54+
if self.dataset_collection_name is None:
55+
raise ValueError("Dataset collection name must be provided for running an evaluation.")
56+
57+
# Create or retrieve the dataset collection
58+
self._am_dataset_collection = self.client.datasets.collection.retrieve(name=self.dataset_collection_name)
59+
60+
# Create or retrieve the evaluation
61+
self._eval = self.client.evaluations.create(
62+
name=self.evaluation_name,
63+
application_id=self._am_app.id,
64+
model_id=self._am_model.id,
65+
dataset_collection_id=self._am_dataset_collection.id
66+
)
67+
68+
def _run_eval(self, func, args, kwargs):
69+
# Create an evaluation run
70+
eval_run = self.client.evaluations.run.create(
71+
evaluation_id=self._eval.id,
72+
metrics_config=self.config,
73+
)
74+
# Get all records from the datasets
75+
dataset_collection_records = []
76+
for dataset_id in self._am_dataset_collection.dataset_ids:
77+
dataset_records = self.client.datasets.records.list(sha=dataset_id)
78+
dataset_collection_records.extend(dataset_records)
79+
results = []
80+
for record in dataset_collection_records:
81+
result = func(record['context_docs'], record['user_query'], *args, **kwargs)
82+
payload = {
83+
"application_id": self._am_app.id,
84+
"version": self._am_app.version,
85+
"prompt": record['prompt'] or "",
86+
"user_query": record['user_query'] or "",
87+
"context_docs": [d for d in record['context_docs']],
88+
"output": result,
89+
"evaluation_id": self._eval.id,
90+
"evaluation_run_id": eval_run.id,
91+
}
92+
results.append((result, self.client.analyze.create(body=[payload])))
93+
return results
94+
95+
def _run_production_analysis(self, func, context, sys_prompt, user_query, args, kwargs):
96+
result = func(context, sys_prompt, user_query, *args, **kwargs)
97+
if result is None:
98+
raise ValueError("Result must be returned by the decorated function")
99+
payload = {
100+
"application_id": self._am_app.id,
101+
"version": self._am_app.version,
102+
"prompt": sys_prompt or "",
103+
"user_query": user_query or "",
104+
"context_docs": context or [],
105+
"output": result
106+
}
107+
aimon_response = self.client.analyze.create(body=[payload])
108+
return aimon_response, result
109+
110+
def __call__(self, func):
111+
@wraps(func)
112+
def wrapper(context=None, sys_prompt=None, user_query=None, *args, **kwargs):
113+
114+
if self.evaluation_name is not None:
115+
return self._run_eval(func, args, kwargs)
116+
else:
117+
# Production mode, run the provided args through the user function
118+
return self._run_production_analysis(func, context, sys_prompt, user_query, args, kwargs)
119+
120+
return wrapper
121+
122+
123+
analyze = Analyze

aimon/decorators/common.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
from aimon import Client
3+
4+
5+
# A singleton class that instantiates the Aimon client once
6+
# and provides a method to get the client instance
7+
class AimonClientSingleton:
8+
_instance = None
9+
10+
@staticmethod
11+
def get_instance(api_key=None):
12+
if AimonClientSingleton._instance is None:
13+
api_key = os.getenv('AIMON_API_KEY') if not api_key else api_key
14+
AimonClientSingleton._instance = Client(auth_header="Bearer {}".format(api_key))
15+
return AimonClientSingleton._instance

aimon/decorators/detect.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from functools import wraps
2+
3+
from .common import AimonClientSingleton
4+
5+
6+
class DetectWithQueryFuncReturningContext(object):
7+
DEFAULT_CONFIG = {'hallucination': {'detector_name': 'default'}}
8+
9+
def __init__(self, api_key=None, config=None):
10+
self.client = AimonClientSingleton.get_instance(api_key)
11+
self.config = config if config else self.DEFAULT_CONFIG
12+
13+
def __call__(self, func):
14+
@wraps(func)
15+
def wrapper(user_query, *args, **kwargs):
16+
result, context = func(user_query, *args, **kwargs)
17+
18+
if result is None or context is None:
19+
raise ValueError("Result and context must be returned by the decorated function")
20+
21+
data_to_send = [{
22+
"user_query": user_query,
23+
"context": context,
24+
"generated_text": result,
25+
"config": self.config
26+
}]
27+
28+
aimon_response = self.client.inference.detect(body=data_to_send)[0]
29+
return result, context, aimon_response
30+
31+
return wrapper
32+
33+
34+
class DetectWithQueryInstructionsFuncReturningContext(DetectWithQueryFuncReturningContext):
35+
def __call__(self, func):
36+
@wraps(func)
37+
def wrapper(user_query, instructions, *args, **kwargs):
38+
result, context = func(user_query, instructions, *args, **kwargs)
39+
40+
if result is None or context is None:
41+
raise ValueError("Result and context must be returned by the decorated function")
42+
43+
data_to_send = [{
44+
"user_query": user_query,
45+
"context": context,
46+
"generated_text": result,
47+
"instructions": instructions,
48+
"config": self.config
49+
}]
50+
51+
aimon_response = self.client.inference.detect(body=data_to_send)[0]
52+
return result, context, aimon_response
53+
54+
return wrapper
55+
56+
57+
# Another class but does not include instructions in the wrapper call
58+
class DetectWithContextQuery(object):
59+
DEFAULT_CONFIG = {'hallucination': {'detector_name': 'default'}}
60+
61+
def __init__(self, api_key=None, config=None):
62+
self.client = AimonClientSingleton.get_instance(api_key)
63+
self.config = config if config else self.DEFAULT_CONFIG
64+
65+
def __call__(self, func):
66+
@wraps(func)
67+
def wrapper(context, user_query, *args, **kwargs):
68+
result = func(context, user_query, *args, **kwargs)
69+
70+
if result is None:
71+
raise ValueError("Result must be returned by the decorated function")
72+
73+
data_to_send = [{
74+
"context": context,
75+
"user_query": user_query,
76+
"generated_text": result,
77+
"config": self.config
78+
}]
79+
80+
aimon_response = self.client.inference.detect(body=data_to_send)[0]
81+
return result, aimon_response
82+
83+
return wrapper
84+
85+
86+
class DetectWithContextQueryInstructions(DetectWithContextQuery):
87+
def __call__(self, func):
88+
@wraps(func)
89+
def wrapper(context, user_query, instructions, *args, **kwargs):
90+
result = func(context, user_query, instructions, *args, **kwargs)
91+
92+
if result is None:
93+
raise ValueError("Result must be returned by the decorated function")
94+
95+
data_to_send = [{
96+
"context": context,
97+
"user_query": user_query,
98+
"generated_text": result,
99+
"instructions": instructions,
100+
"config": self.config
101+
}]
102+
103+
aimon_response = self.client.inference.detect(body=data_to_send)[0]
104+
return result, aimon_response
105+
106+
return wrapper
107+

0 commit comments

Comments
 (0)