celai 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cel/__init__.py +0 -0
- cel/assets/celia_logo.png +0 -0
- cel/assistants/base_assistant.py +227 -0
- cel/assistants/common.py +28 -0
- cel/assistants/function_context.py +24 -0
- cel/assistants/function_response.py +23 -0
- cel/assistants/macaw/custom_chat_models/chat_open_router.py +53 -0
- cel/assistants/macaw/macaw_assistant.py +203 -0
- cel/assistants/macaw/macaw_history_adapter.py +33 -0
- cel/assistants/macaw/macaw_inference_context.py +28 -0
- cel/assistants/macaw/macaw_nlp.py +281 -0
- cel/assistants/macaw/macaw_settings.py +49 -0
- cel/assistants/macaw/macaw_test_chat_model.py +37 -0
- cel/assistants/macaw/macaw_utils.py +42 -0
- cel/assistants/stream_content_chunk.py +21 -0
- cel/cache.py +23 -0
- cel/comms/client_command.py +32 -0
- cel/comms/sentense_detection.py +116 -0
- cel/comms/utils.py +46 -0
- cel/config.py +15 -0
- cel/connectors/cli/cli_connector.py +169 -0
- cel/connectors/cli/model/cli_lead.py +40 -0
- cel/connectors/cli/model/cli_message.py +49 -0
- cel/connectors/telegram/__init__.py +4 -0
- cel/connectors/telegram/model/telegram_attachment.py +173 -0
- cel/connectors/telegram/model/telegram_lead.py +46 -0
- cel/connectors/telegram/model/telegram_message.py +46 -0
- cel/connectors/telegram/samples/hi.mp3 +0 -0
- cel/connectors/telegram/samples/hi.oga +0 -0
- cel/connectors/telegram/samples/hi.ogg +0 -0
- cel/connectors/telegram/samples/message.json +21 -0
- cel/connectors/telegram/samples/message_audio.json +27 -0
- cel/connectors/telegram/samples/message_callback_query.json +50 -0
- cel/connectors/telegram/samples/message_img.json +44 -0
- cel/connectors/telegram/samples/message_imgx2.json +44 -0
- cel/connectors/telegram/samples/message_location.json +24 -0
- cel/connectors/telegram/samples/message_location_place.json +34 -0
- cel/connectors/telegram/samples/sample_message.json +21 -0
- cel/connectors/telegram/stt_utils.py +53 -0
- cel/connectors/telegram/telegram_connector.py +263 -0
- cel/connectors/vapi/README.md +15 -0
- cel/connectors/vapi/model/vapi_lead.py +63 -0
- cel/connectors/vapi/model/vapi_message.py +59 -0
- cel/connectors/vapi/sample_data/chunk_vapi_1.json +21 -0
- cel/connectors/vapi/sample_data/chunk_vapi_2.json +21 -0
- cel/connectors/vapi/sample_data/chunk_vapi_3.json +21 -0
- cel/connectors/vapi/sample_data/chunk_vapi_last.json +21 -0
- cel/connectors/vapi/sample_data/request.json +31 -0
- cel/connectors/vapi/utils.py +26 -0
- cel/connectors/vapi/vapi_connector.py +162 -0
- cel/connectors/whatsapp/__init__.py +4 -0
- cel/connectors/whatsapp/components/component.py +7 -0
- cel/connectors/whatsapp/components/document.py +43 -0
- cel/connectors/whatsapp/components/document_url.py +40 -0
- cel/connectors/whatsapp/components/image.py +50 -0
- cel/connectors/whatsapp/components/reply_button.py +31 -0
- cel/connectors/whatsapp/components/text.py +28 -0
- cel/connectors/whatsapp/components/utils.py +14 -0
- cel/connectors/whatsapp/components/video.py +49 -0
- cel/connectors/whatsapp/constants.py +1 -0
- cel/connectors/whatsapp/functions/buttons.py +114 -0
- cel/connectors/whatsapp/functions/utils.py +328 -0
- cel/connectors/whatsapp/model/media_utils.py +143 -0
- cel/connectors/whatsapp/model/samples/img_caption_message.json +42 -0
- cel/connectors/whatsapp/model/samples/img_message.json +42 -0
- cel/connectors/whatsapp/model/samples/react_message.json +40 -0
- cel/connectors/whatsapp/model/samples/text_message.json +39 -0
- cel/connectors/whatsapp/model/whatsapp_attachment.py +254 -0
- cel/connectors/whatsapp/model/whatsapp_lead.py +51 -0
- cel/connectors/whatsapp/model/whatsapp_message.py +128 -0
- cel/connectors/whatsapp/phone_utils.py +10 -0
- cel/connectors/whatsapp/utils.py +6 -0
- cel/connectors/whatsapp/whatsapp_connector.py +485 -0
- cel/decoders/gcp_geocoding.py +30 -0
- cel/gateway/__init__.py +2 -0
- cel/gateway/message_gateway.py +401 -0
- cel/gateway/model/__init__.py +6 -0
- cel/gateway/model/attachment.py +93 -0
- cel/gateway/model/base_connector.py +36 -0
- cel/gateway/model/conversation_lead.py +46 -0
- cel/gateway/model/conversation_peer.py +42 -0
- cel/gateway/model/message.py +41 -0
- cel/gateway/model/message_gateway_context.py +10 -0
- cel/gateway/model/outgoing/__init__.py +5 -0
- cel/gateway/model/outgoing/outgoing_message.py +54 -0
- cel/gateway/model/outgoing/outgoing_message_factory.py +38 -0
- cel/gateway/model/outgoing/outgoing_message_link.py +62 -0
- cel/gateway/model/outgoing/outgoing_message_select.py +64 -0
- cel/gateway/model/outgoing/outgoing_message_text.py +48 -0
- cel/gateway/request_context.py +47 -0
- cel/message_enhancers/default_message_enhancer.py +23 -0
- cel/message_enhancers/smart_message_enhancer_openai.py +115 -0
- cel/middlewares/__init__.py +6 -0
- cel/middlewares/deepgram_stt.py +63 -0
- cel/middlewares/geodecoding.py +48 -0
- cel/middlewares/in_mem_blacklist.py +43 -0
- cel/middlewares/redis_blacklist.py +44 -0
- cel/middlewares/redis_blacklist_async.py +44 -0
- cel/middlewares/session_middleware.py +141 -0
- cel/model/common.py +37 -0
- cel/prompt/prompt_template.py +67 -0
- cel/rag/__init__.py +0 -0
- cel/rag/providers/markdown_rag.py +118 -0
- cel/rag/providers/rag_retriever.py +15 -0
- cel/rag/slicers/__init__.py +0 -0
- cel/rag/slicers/base_slicer.py +37 -0
- cel/rag/slicers/markdown/__init__.py +1 -0
- cel/rag/slicers/markdown/markdown.py +117 -0
- cel/rag/slicers/markdown/utils.py +74 -0
- cel/rag/slicers/utils.py +0 -0
- cel/rag/stores/chroma/chroma_store.py +113 -0
- cel/rag/stores/vector_store.py +57 -0
- cel/rag/text2vec/cached_openai.py +76 -0
- cel/rag/text2vec/utils.py +19 -0
- cel/stores/common/async_cache_aside_redis.py +146 -0
- cel/stores/common/cache_aside_redis.py +162 -0
- cel/stores/common/key_value_store.py +45 -0
- cel/stores/common/list_redis_store_async.py +36 -0
- cel/stores/common/memory_cache.py +82 -0
- cel/stores/history/base_history_provider.py +24 -0
- cel/stores/history/history_inmemory_provider.py +50 -0
- cel/stores/history/history_redis_provider.py +57 -0
- cel/stores/history/history_redis_provider_async.py +55 -0
- cel/stores/state/base_state_provider.py +31 -0
- cel/stores/state/state_inmemory_provider.py +43 -0
- cel/stores/state/state_redis_provider.py +58 -0
- cel/voice/deepgram_adapter.py +34 -0
- celai-0.1.0.dist-info/METADATA +95 -0
- celai-0.1.0.dist-info/RECORD +130 -0
- celai-0.1.0.dist-info/WHEEL +4 -0
cel/__init__.py
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from loguru import logger as log
|
|
5
|
+
from cel.assistants.function_context import FunctionContext
|
|
6
|
+
from cel.assistants.function_response import FunctionResponse
|
|
7
|
+
from cel.gateway.model.base_connector import BaseConnector
|
|
8
|
+
from cel.assistants.common import EventResponse, FunctionDefinition
|
|
9
|
+
from cel.gateway.model.message import Message, ConversationLead
|
|
10
|
+
from cel.prompt.prompt_template import PromptTemplate
|
|
11
|
+
from cel.rag.providers.rag_retriever import RAGRetriever
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Events:
|
|
15
|
+
START: str = "start"
|
|
16
|
+
NEW_MESSAGE: str = "new_message"
|
|
17
|
+
IMAGE: str = "image"
|
|
18
|
+
AUDIO: str = "audio"
|
|
19
|
+
END: str = "end"
|
|
20
|
+
|
|
21
|
+
class BaseAssistant(ABC):
|
|
22
|
+
|
|
23
|
+
def __init__(self, prompt: PromptTemplate = None):
|
|
24
|
+
|
|
25
|
+
self.function_handlers = {}
|
|
26
|
+
self.event_handlers = {}
|
|
27
|
+
self.client_commands_handlers = {}
|
|
28
|
+
self.timeout_handlers = {}
|
|
29
|
+
self.rag_retriever: RAGRetriever = None
|
|
30
|
+
if prompt:
|
|
31
|
+
assert isinstance(prompt, PromptTemplate), "prompt must be an instance of PromptTemplate"
|
|
32
|
+
self.prompt = prompt or PromptTemplate('')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def set_rag_retrieval(self, rag_retrieval: RAGRetriever):
|
|
37
|
+
# check type
|
|
38
|
+
assert isinstance(rag_retrieval, RAGRetriever), "rag_retrieval must be an instance of RAGRetriever"
|
|
39
|
+
self.rag_retriever = rag_retrieval
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def function(self, name, desc, params):
|
|
43
|
+
def decorator(func):
|
|
44
|
+
self.function_handlers[name] = {
|
|
45
|
+
# 'desc': desc,
|
|
46
|
+
# 'params': params,
|
|
47
|
+
'definition': FunctionDefinition(name=name, description=desc, parameters=params),
|
|
48
|
+
'func': func
|
|
49
|
+
}
|
|
50
|
+
def wrapper(*args, **kwargs):
|
|
51
|
+
return func(*args, **kwargs)
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
return decorator
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def event(self, name: Events | str):
|
|
58
|
+
def decorator(func):
|
|
59
|
+
self.event_handlers[name] = {
|
|
60
|
+
'func': func
|
|
61
|
+
}
|
|
62
|
+
def wrapper(*args, **kwargs):
|
|
63
|
+
return func(*args, **kwargs)
|
|
64
|
+
|
|
65
|
+
return wrapper
|
|
66
|
+
return decorator
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def client_command(self, name):
|
|
70
|
+
def decorator(func):
|
|
71
|
+
self.client_commands_handlers[name] = {
|
|
72
|
+
'func': func
|
|
73
|
+
}
|
|
74
|
+
def wrapper(*args, **kwargs):
|
|
75
|
+
return func(*args, **kwargs)
|
|
76
|
+
|
|
77
|
+
return wrapper
|
|
78
|
+
return decorator
|
|
79
|
+
|
|
80
|
+
def timeout(self, label):
|
|
81
|
+
def decorator(func):
|
|
82
|
+
self.timeout_handlers[label] = {
|
|
83
|
+
'func': func
|
|
84
|
+
}
|
|
85
|
+
def wrapper(*args, **kwargs):
|
|
86
|
+
return func(*args, **kwargs)
|
|
87
|
+
|
|
88
|
+
return wrapper
|
|
89
|
+
return decorator
|
|
90
|
+
|
|
91
|
+
async def eval_client_command(self, command: str, args: list[str], message: Message):
|
|
92
|
+
"""Evaluate the client command and call the respective handler"""
|
|
93
|
+
lead = message.lead
|
|
94
|
+
connector = lead.connector
|
|
95
|
+
|
|
96
|
+
if command in self.client_commands_handlers:
|
|
97
|
+
func = self.client_commands_handlers[command]['func']
|
|
98
|
+
# Build args_dict
|
|
99
|
+
from cel.gateway.request_context import RequestContext
|
|
100
|
+
ctx = RequestContext(lead=lead,
|
|
101
|
+
connector=connector,
|
|
102
|
+
assistant=self)
|
|
103
|
+
args_dict = {
|
|
104
|
+
'session': lead.get_session_id(),
|
|
105
|
+
'ctx': ctx,
|
|
106
|
+
'lead': lead,
|
|
107
|
+
'message': None,
|
|
108
|
+
'connector': connector,
|
|
109
|
+
'command': command,
|
|
110
|
+
'args': args
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Build kwargs using args and args_dict
|
|
114
|
+
args = inspect.getfullargspec(func).args
|
|
115
|
+
kwargs = {}
|
|
116
|
+
for arg in args:
|
|
117
|
+
if arg in args_dict:
|
|
118
|
+
kwargs[arg] = args_dict[arg]
|
|
119
|
+
|
|
120
|
+
# Check if the function is a coroutine (async function)
|
|
121
|
+
if inspect.iscoroutinefunction(func):
|
|
122
|
+
await func(**kwargs)
|
|
123
|
+
else:
|
|
124
|
+
func(**kwargs)
|
|
125
|
+
return False
|
|
126
|
+
else:
|
|
127
|
+
log.warning(f"Client command {command} not found in assistant implementation")
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
async def call_event(self, event_name: str, lead: ConversationLead, message: Message = None, connector: BaseConnector = None, data: dict= None) -> EventResponse:
|
|
131
|
+
"""Call the respective event handler, if exists"""
|
|
132
|
+
if event_name in self.event_handlers:
|
|
133
|
+
func = self.event_handlers[event_name]['func']
|
|
134
|
+
connector = connector or lead.connector
|
|
135
|
+
from cel.gateway.request_context import RequestContext
|
|
136
|
+
ctx = RequestContext(lead=lead,
|
|
137
|
+
message=message,
|
|
138
|
+
assistant=self,
|
|
139
|
+
connector=connector)
|
|
140
|
+
|
|
141
|
+
# Build args_dict
|
|
142
|
+
args_dict = {
|
|
143
|
+
'session': lead.get_session_id(),
|
|
144
|
+
'ctx': ctx,
|
|
145
|
+
'lead': lead,
|
|
146
|
+
'message': message,
|
|
147
|
+
'connector': connector,
|
|
148
|
+
'data': data
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Build kwargs using args and args_dict
|
|
152
|
+
args = inspect.getfullargspec(func).args
|
|
153
|
+
kwargs = {}
|
|
154
|
+
for arg in args:
|
|
155
|
+
if arg in args_dict:
|
|
156
|
+
kwargs[arg] = args_dict[arg]
|
|
157
|
+
|
|
158
|
+
# Check if the function is a coroutine (async function)
|
|
159
|
+
response = None
|
|
160
|
+
if inspect.iscoroutinefunction(func):
|
|
161
|
+
response = await func(**kwargs)
|
|
162
|
+
else:
|
|
163
|
+
response = func(**kwargs)
|
|
164
|
+
|
|
165
|
+
assert isinstance(response, EventResponse) or response is None, "Event handler must return an instance of EventResponse or None"
|
|
166
|
+
return response
|
|
167
|
+
else:
|
|
168
|
+
# raise ValueError(f"Event {event_name} not found")
|
|
169
|
+
log.error(f"Event {event_name} not found")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def call_function(self, func_name: str, params: dict, lead: ConversationLead) -> FunctionResponse:
|
|
173
|
+
"""Call the respective function handler, if exists. Map function arguments to the function handler signature"""
|
|
174
|
+
connector = lead.connector
|
|
175
|
+
if func_name in self.function_handlers:
|
|
176
|
+
func = self.function_handlers[func_name]['func']
|
|
177
|
+
args = inspect.getfullargspec(func).args
|
|
178
|
+
|
|
179
|
+
ctx = FunctionContext(lead=lead, connector=connector)
|
|
180
|
+
args_dict = {
|
|
181
|
+
'session': lead.get_session_id(),
|
|
182
|
+
'ctx': ctx,
|
|
183
|
+
'params': params,
|
|
184
|
+
'lead': lead,
|
|
185
|
+
'message': None,
|
|
186
|
+
'connector': connector
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Build kwargs using args and args_dict
|
|
190
|
+
kwargs = {}
|
|
191
|
+
for arg in args:
|
|
192
|
+
if arg in args_dict:
|
|
193
|
+
kwargs[arg] = args_dict[arg]
|
|
194
|
+
|
|
195
|
+
# Check if the function is a coroutine (async function)
|
|
196
|
+
response = None
|
|
197
|
+
if inspect.iscoroutinefunction(func):
|
|
198
|
+
response = await func(**kwargs)
|
|
199
|
+
else:
|
|
200
|
+
response = func(**kwargs)
|
|
201
|
+
|
|
202
|
+
assert isinstance(response, FunctionResponse) or response is None, "Function handler must return an instance of CommandResponse or None"
|
|
203
|
+
return response
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
raise ValueError(f"Function {func_name} not found")
|
|
207
|
+
|
|
208
|
+
@abstractmethod
|
|
209
|
+
async def new_message(self, lead: ConversationLead, message: str, local_state: dict = {}):
|
|
210
|
+
raise NotImplementedError
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@abstractmethod
|
|
214
|
+
async def blend(self, lead: ConversationLead, text: str, history_length: int = 3):
|
|
215
|
+
raise NotImplementedError
|
|
216
|
+
|
|
217
|
+
@abstractmethod
|
|
218
|
+
async def process_client_command(self, lead: ConversationLead, command: str, args: list[str]):
|
|
219
|
+
yield "Command not found"
|
|
220
|
+
|
|
221
|
+
def get_functions(self) -> list[FunctionDefinition]:
|
|
222
|
+
return [f['definition'] for f in self.function_handlers.values()]
|
|
223
|
+
|
|
224
|
+
def get_events(self):
|
|
225
|
+
return self.event_handlers
|
|
226
|
+
|
|
227
|
+
|
cel/assistants/common.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Param(ABC):
|
|
8
|
+
name: str
|
|
9
|
+
type: str
|
|
10
|
+
description: str
|
|
11
|
+
required: bool = True
|
|
12
|
+
enum: list[str] = None
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class FunctionDefinition(ABC):
|
|
16
|
+
name: str
|
|
17
|
+
description: str
|
|
18
|
+
parameters: list[Param]
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class EventResponse(ABC):
|
|
22
|
+
text: str = None
|
|
23
|
+
image: str = None
|
|
24
|
+
audio: str = None
|
|
25
|
+
video: str = None
|
|
26
|
+
disable_ai_response: bool = False
|
|
27
|
+
blend: bool = False
|
|
28
|
+
is_private: bool = False
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from cel.assistants.function_response import FunctionResponse, RequestMode
|
|
2
|
+
from cel.gateway.model.base_connector import BaseConnector
|
|
3
|
+
from cel.gateway.model.message import ConversationLead, Message
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class FunctionContext:
|
|
11
|
+
lead: ConversationLead
|
|
12
|
+
message: Message | None = None
|
|
13
|
+
connector: BaseConnector | None = None
|
|
14
|
+
state: dict | None = field(default_factory=dict)
|
|
15
|
+
history: list[dict] | None = field(default_factory=list)
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def response_text(text: str, request_mode: RequestMode = RequestMode.SINGLE):
|
|
19
|
+
return FunctionResponse(text=text, request_mode=request_mode)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# @staticmethod
|
|
23
|
+
# def response_image(image_url: str, request_mode: RequestMode = RequestMode.SINGLE):
|
|
24
|
+
# return FunctionResponse(image_url=image_url, request_mode=request_mode)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RequestMode:
|
|
6
|
+
STREAM = "stream"
|
|
7
|
+
SINGLE = "single"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class FunctionResponse:
|
|
12
|
+
text: str
|
|
13
|
+
|
|
14
|
+
""" the execution function response will be sent as a stream of chunks or as a single response """
|
|
15
|
+
request_mode: str = RequestMode.SINGLE
|
|
16
|
+
|
|
17
|
+
""" If you want to disable direct response from Asiistant AI, use callback to handle the response.
|
|
18
|
+
It is useful when you want to handle the response in a different way, like sending the response to a different service.
|
|
19
|
+
Another use case is when oyu need to use specific platform features to send the response, like interactives messages, buttons, etc.
|
|
20
|
+
"""
|
|
21
|
+
callback: Callable = None
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# https://medium.com/@gal.peretz/openrouter-langchain-leverage-opensource-models-without-the-ops-hassle-9ffbf0016da7
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from langchain_community.chat_models import ChatOpenAI
|
|
6
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
OPENROUTER_API_BASE = "https://openrouter.ai/api/v1"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# TODO: OpenRouter Models has problems with Langchain tooling
|
|
13
|
+
# This LLM model wrapper throws an error when used with Langchain tooling
|
|
14
|
+
# specifically with bind_tools. The error is due to the fact that the
|
|
15
|
+
# OpenRouter models do not support tooling??? I am not sure about this.
|
|
16
|
+
# --------------------------------------------------------------------
|
|
17
|
+
class ChatOpenRouter(ChatOpenAI):
|
|
18
|
+
openai_api_base: str
|
|
19
|
+
openai_api_key: str
|
|
20
|
+
model_name: str
|
|
21
|
+
|
|
22
|
+
def __init__(self,
|
|
23
|
+
model: str,
|
|
24
|
+
openai_api_key: Optional[str] = None,
|
|
25
|
+
openai_api_base: str = OPENROUTER_API_BASE,
|
|
26
|
+
**kwargs):
|
|
27
|
+
openai_api_key = openai_api_key or os.getenv('OPENROUTER_API_KEY')
|
|
28
|
+
super().__init__(openai_api_base=openai_api_base,
|
|
29
|
+
openai_api_key=openai_api_key,
|
|
30
|
+
model_name=model, **kwargs)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ChatOpenAIOpenRouter(**kwargs):
|
|
34
|
+
return ChatOpenAI(
|
|
35
|
+
**kwargs,
|
|
36
|
+
openai_api_base=OPENROUTER_API_BASE,
|
|
37
|
+
openai_api_key = os.getenv('OPENROUTER_API_KEY')
|
|
38
|
+
)
|
|
39
|
+
# --------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# if __name__ == '__main__':
|
|
43
|
+
|
|
44
|
+
# # Load .env variables
|
|
45
|
+
# from dotenv import load_dotenv
|
|
46
|
+
# load_dotenv()
|
|
47
|
+
|
|
48
|
+
# llm = ChatOpenRouter(
|
|
49
|
+
# model="mistralai/mixtral-8x7b-instruct"
|
|
50
|
+
# )
|
|
51
|
+
# prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
|
|
52
|
+
# openrouter_chain = prompt | llm
|
|
53
|
+
# print(openrouter_chain.invoke({"topic": "banana"}))
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from loguru import logger as log
|
|
3
|
+
from cel.assistants.base_assistant import BaseAssistant
|
|
4
|
+
from cel.assistants.macaw.macaw_inference_context import MacawNlpInferenceContext
|
|
5
|
+
from cel.assistants.macaw.macaw_nlp import MacawFunctionCall, blend_message, process_insights, process_new_message
|
|
6
|
+
from cel.assistants.macaw.macaw_settings import MacawSettings
|
|
7
|
+
from cel.gateway.model.conversation_lead import ConversationLead
|
|
8
|
+
from cel.prompt.prompt_template import PromptTemplate
|
|
9
|
+
from cel.stores.history.base_history_provider import BaseHistoryProvider
|
|
10
|
+
from cel.stores.history.history_inmemory_provider import InMemoryHistoryProvider
|
|
11
|
+
from cel.stores.state.base_state_provider import BaseChatStateProvider
|
|
12
|
+
from cel.stores.state.state_inmemory_provider import InMemoryStateProvider
|
|
13
|
+
from langchain.load.load import loads, load
|
|
14
|
+
from langchain.load.dump import dumps
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MacawAssistant(BaseAssistant):
|
|
18
|
+
"""Macaw Assistant is a conversational assistant that uses Langchain as core NLP.
|
|
19
|
+
This assistant perform different NLP tasks like blending, insights, and more.
|
|
20
|
+
To perform these tasks, Macaw Assistant uses differents models and tools. You can
|
|
21
|
+
configure the assistant with different settings and tools for each task.
|
|
22
|
+
|
|
23
|
+
If you use this assistant make sure to setup your LLM API key in the environment variable:
|
|
24
|
+
- OPENAI_API_KEY: The OpenAI API key for the assistant.
|
|
25
|
+
|
|
26
|
+
Tasks:
|
|
27
|
+
- Blending: Blend a message with the current context.
|
|
28
|
+
- Insights: Get insights from the current context.
|
|
29
|
+
- Process new message: Process a new message with the current context and return the response.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
state (dict, optional): Initial state for the assistant. Defaults to None.
|
|
33
|
+
history_store (BaseHistoryProvider, optional): History store provider. Defaults to None.
|
|
34
|
+
state_store (BaseChatStateProvider, optional): State store provider. Defaults to None.
|
|
35
|
+
prompt (PromptTemplate, optional): Prompt template for the assistant. Defaults to None.
|
|
36
|
+
insight_targets (dict, optional): Insight targets for the assistant. Defaults to {}.
|
|
37
|
+
settings (MacawSettings, optional): Settings for the assistant. Defaults to None.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self,
|
|
42
|
+
state: dict = None,
|
|
43
|
+
history_store:BaseHistoryProvider=None,
|
|
44
|
+
state_store: BaseChatStateProvider=None,
|
|
45
|
+
prompt: PromptTemplate = None,
|
|
46
|
+
insight_targets: dict = {},
|
|
47
|
+
settings: MacawSettings = None,
|
|
48
|
+
llm=None,
|
|
49
|
+
):
|
|
50
|
+
|
|
51
|
+
super().__init__(prompt=prompt)
|
|
52
|
+
self.state = state or {}
|
|
53
|
+
self.insight_targets = insight_targets
|
|
54
|
+
self._state_store = state_store or InMemoryStateProvider()
|
|
55
|
+
self._history_store = history_store or InMemoryHistoryProvider()
|
|
56
|
+
if settings is None:
|
|
57
|
+
log.warning("No settings provided for Macaw Assistant, using default settings")
|
|
58
|
+
self.settings = settings or MacawSettings()
|
|
59
|
+
self.llm = llm
|
|
60
|
+
log.debug(f"Macaw Assistant initialized with settings: {self.settings}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def new_message(self, lead: ConversationLead, message: str, local_state: dict = {}):
|
|
64
|
+
# create context
|
|
65
|
+
ctx = MacawNlpInferenceContext(
|
|
66
|
+
lead=lead,
|
|
67
|
+
prompt=self.prompt,
|
|
68
|
+
init_state=self.state,
|
|
69
|
+
local_state=local_state,
|
|
70
|
+
functions=self.get_functions(),
|
|
71
|
+
history_store=self._history_store,
|
|
72
|
+
state_store=self._state_store,
|
|
73
|
+
settings=self.settings,
|
|
74
|
+
rag_retriever=self.rag_retriever,
|
|
75
|
+
llm=self.llm
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def on_function_call(ctx: MacawNlpInferenceContext, call: MacawFunctionCall):
|
|
79
|
+
return await self.call_function(
|
|
80
|
+
func_name=call.name,
|
|
81
|
+
params=call.args,
|
|
82
|
+
lead=ctx.lead
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
# stream this message content from string
|
|
86
|
+
async for chunk in process_new_message(ctx, message, on_function_call):
|
|
87
|
+
yield chunk
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
log.error(f"Macaw Assistant: error processing new message: {e}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def blend(self, lead: ConversationLead, text: str, history_length: int = None):
|
|
94
|
+
# create context
|
|
95
|
+
ctx = MacawNlpInferenceContext(
|
|
96
|
+
lead=lead,
|
|
97
|
+
prompt=self.prompt,
|
|
98
|
+
init_state=self.state,
|
|
99
|
+
local_state={},
|
|
100
|
+
history_store=self._history_store,
|
|
101
|
+
state_store=self._state_store,
|
|
102
|
+
settings=self.settings
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
return await blend_message(ctx, message=text)
|
|
107
|
+
|
|
108
|
+
async def do_insights(self, lead: ConversationLead, targets: dict = {}, history_length: int = 10):
|
|
109
|
+
assert isinstance(targets, dict), "targets must be a dictionary"
|
|
110
|
+
|
|
111
|
+
if self.settings.insights_enabled is False:
|
|
112
|
+
log.warning("Insights are disabled, returning None")
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
log.debug(f"Getting insights for lead: {lead} with targets: {targets}")
|
|
116
|
+
|
|
117
|
+
mix_targets = {**self.insight_targets, **targets}
|
|
118
|
+
|
|
119
|
+
# create context
|
|
120
|
+
ctx = MacawNlpInferenceContext(
|
|
121
|
+
lead=lead,
|
|
122
|
+
prompt=self.prompt,
|
|
123
|
+
init_state=self.state,
|
|
124
|
+
local_state={},
|
|
125
|
+
history_store=self._history_store,
|
|
126
|
+
state_store=self._state_store,
|
|
127
|
+
settings=self.settings
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
insights = await process_insights(ctx, targets=mix_targets)
|
|
131
|
+
|
|
132
|
+
# raise insight event
|
|
133
|
+
await self.call_event("insigths", lead, data=insights)
|
|
134
|
+
return insights
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def process_client_command(self, lead: ConversationLead, command: str, args: list[str]):
|
|
138
|
+
|
|
139
|
+
if command == "reset":
|
|
140
|
+
|
|
141
|
+
if args and args[0] == "all":
|
|
142
|
+
await self._history_store.clear_history(lead.get_session_id())
|
|
143
|
+
yield "History cleared"
|
|
144
|
+
self._state_store.set_store(lead.get_session_id(), {})
|
|
145
|
+
yield "State cleared"
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
await self._history_store.clear_history(lead.get_session_id())
|
|
149
|
+
yield "History cleared"
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if command == "state":
|
|
153
|
+
state = self._state_store.get_store(lead.get_session_id())
|
|
154
|
+
if state is None:
|
|
155
|
+
yield "No state found"
|
|
156
|
+
return
|
|
157
|
+
for k, v in state.items():
|
|
158
|
+
yield f"{k}: {v}"
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if command == "history":
|
|
162
|
+
history = await self._history_store.get_history(lead.get_session_id()) or []
|
|
163
|
+
|
|
164
|
+
if history is None or len(history) == 0:
|
|
165
|
+
yield "History is empty"
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
for h in history:
|
|
169
|
+
aux = load(h)
|
|
170
|
+
log.debug(f"History: {aux}")
|
|
171
|
+
yield dumps(aux)
|
|
172
|
+
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if command == "set":
|
|
176
|
+
if len(args) < 2:
|
|
177
|
+
yield "Not enough arguments"
|
|
178
|
+
return
|
|
179
|
+
key = args[0]
|
|
180
|
+
value = args[1]
|
|
181
|
+
state = self._state_store.get_store(lead.get_session_id())
|
|
182
|
+
state[key] = value
|
|
183
|
+
self._state_store.set_store(lead.get_session_id(), state)
|
|
184
|
+
yield f"State updated: {key}: {value}"
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if command == "prompt":
|
|
188
|
+
yield "Macaw Assistant v0.1"
|
|
189
|
+
|
|
190
|
+
if args and args[0] == "all":
|
|
191
|
+
prompt = self.prompt
|
|
192
|
+
# split into 250 chars aprox chunks
|
|
193
|
+
for i in range(0, len(prompt), 250):
|
|
194
|
+
yield prompt[i:i+250]
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# first 250 chars
|
|
198
|
+
yield self.prompt[:250]
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from cel.gateway.model.conversation_lead import ConversationLead
|
|
2
|
+
from langchain.load.dump import dumpd
|
|
3
|
+
from langchain.load.load import load
|
|
4
|
+
from langchain_core.messages import BaseMessage
|
|
5
|
+
from cel.stores.history.base_history_provider import BaseHistoryProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MacawHistoryAdapter:
|
|
9
|
+
def __init__(self, store: BaseHistoryProvider):
|
|
10
|
+
self.store = store
|
|
11
|
+
|
|
12
|
+
async def append_to_history(self, lead: ConversationLead, entry: BaseMessage, metadata=None, ttl=None):
|
|
13
|
+
assert isinstance(lead, ConversationLead), f"Expected ConversationLead, got {type (lead)}"
|
|
14
|
+
aux = dumpd(entry)
|
|
15
|
+
await self.store.append_to_history(lead.get_session_id(), aux, metadata, ttl)
|
|
16
|
+
|
|
17
|
+
async def get_history(self, lead: ConversationLead) -> list[BaseMessage]:
|
|
18
|
+
assert isinstance(lead, ConversationLead), f"Expected ConversationLead, got {type (lead)}"
|
|
19
|
+
history = await self.store.get_history(lead.get_session_id())
|
|
20
|
+
return [load(h) for h in history]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def clear_history(self, lead: ConversationLead, keep_last_messages=None):
|
|
24
|
+
assert isinstance(lead, ConversationLead), f"Expected ConversationLead, got {type (lead)}"
|
|
25
|
+
await self.store.clear_history(lead.get_session_id(), keep_last_messages)
|
|
26
|
+
|
|
27
|
+
async def get_last_messages(self, lead: ConversationLead, count) -> list[BaseMessage]:
|
|
28
|
+
assert isinstance(lead, ConversationLead), f"Expected ConversationLead, got {type (lead)}"
|
|
29
|
+
msgs = await self.store.get_last_messages(lead.get_session_id(), count)
|
|
30
|
+
return [load(m) for m in msgs]
|
|
31
|
+
|
|
32
|
+
async def close_conversation(self, lead: ConversationLead):
|
|
33
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from cel.assistants.common import FunctionDefinition
|
|
2
|
+
from cel.assistants.macaw.macaw_settings import MacawSettings
|
|
3
|
+
from cel.gateway.model.conversation_lead import ConversationLead
|
|
4
|
+
from cel.prompt.prompt_template import PromptTemplate
|
|
5
|
+
from cel.rag.providers.rag_retriever import RAGRetriever
|
|
6
|
+
from cel.stores.history.base_history_provider import BaseHistoryProvider
|
|
7
|
+
from cel.stores.state.base_state_provider import BaseChatStateProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from abc import ABC
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class MacawNlpInferenceContext(ABC):
|
|
17
|
+
lead: ConversationLead
|
|
18
|
+
settings: MacawSettings
|
|
19
|
+
prompt: PromptTemplate = ''
|
|
20
|
+
init_state: dict = None
|
|
21
|
+
local_state: dict = None
|
|
22
|
+
functions: list[FunctionDefinition] = None
|
|
23
|
+
rag_retriever: RAGRetriever = None
|
|
24
|
+
# default value {} is used to avoid mutable default arguments
|
|
25
|
+
llm_kwargs: dict[str, Any] = None
|
|
26
|
+
history_store: BaseHistoryProvider = None
|
|
27
|
+
state_store: BaseChatStateProvider = None
|
|
28
|
+
llm: Any = None
|