autonomous-app 0.3.18__py3-none-any.whl → 0.3.20__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.
@@ -0,0 +1,299 @@
1
+ import datetime
2
+ import io
3
+ import json
4
+ import os
5
+ import random
6
+ import wave
7
+ from http import client
8
+
9
+ from google import genai
10
+ from google.genai import types
11
+ from PIL import Image as PILImage
12
+ from pydub import AudioSegment
13
+
14
+ from autonomous import log
15
+ from autonomous.model.autoattr import DictAttr, ListAttr, StringAttr
16
+ from autonomous.model.automodel import AutoModel
17
+
18
+
19
+ class GeminiAIModel(AutoModel):
20
+ _client = None
21
+ _text_model = "gemini-3-pro-preview"
22
+ _summary_model = "gemini-2.5-flash"
23
+ _image_model = "gemini-3-pro-image-preview"
24
+ _json_model = "gemini-3-pro-preview"
25
+ _stt_model = "gemini-3-pro-preview"
26
+ _tts_model = "gemini-2.5-flash-preview-tts"
27
+ messages = ListAttr(StringAttr(default=[]))
28
+ name = StringAttr(default="agent")
29
+ instructions = StringAttr(
30
+ default="You are highly skilled AI trained to assist with various tasks."
31
+ )
32
+ description = StringAttr(
33
+ default="A helpful AI assistant trained to assist with various tasks."
34
+ )
35
+
36
+ @property
37
+ def client(self):
38
+ if not self._client:
39
+ # log("=== Initializing Gemini AI Client ===", _print=True)
40
+ self._client = genai.Client(api_key=os.environ.get("GOOGLEAI_KEY"))
41
+ # log("=== Gemini AI Client Initialized ===", _print=True)
42
+ return self._client
43
+
44
+ def _add_function(self, user_function):
45
+ # This function is now a bit more advanced to conform to the Tool Use schema
46
+ tool_schema = {
47
+ "name": user_function.get("name"),
48
+ "description": user_function.get("description"),
49
+ "parameters": user_function.get("parameters"),
50
+ }
51
+
52
+ # Validate that the schema has a name, description, and parameters
53
+ if not all(
54
+ [tool_schema["name"], tool_schema["description"], tool_schema["parameters"]]
55
+ ):
56
+ raise ValueError(
57
+ "Tool schema must have a 'name', 'description', and 'parameters' field."
58
+ )
59
+
60
+ return tool_schema
61
+
62
+ def _create_wav_header(
63
+ self, raw_audio_bytes, channels=1, rate=24000, sample_width=2
64
+ ):
65
+ """Creates an in-memory WAV file from raw PCM audio bytes."""
66
+ buffer = io.BytesIO()
67
+ with wave.open(buffer, "wb") as wav_file:
68
+ # Set audio parameters
69
+ wav_file.setnchannels(channels)
70
+ wav_file.setsampwidth(sample_width)
71
+ wav_file.setframerate(rate) # 16,000 Hz sample rate
72
+
73
+ # Write the raw audio data
74
+ wav_file.writeframes(raw_audio_bytes)
75
+
76
+ buffer.seek(0)
77
+ return buffer
78
+
79
+ def generate_json(self, message, function, additional_instructions=""):
80
+ # The API call must use the 'tools' parameter instead of 'response_json_schema'
81
+ function_definition = self._add_function(function)
82
+
83
+ response = self.client.models.generate_content(
84
+ model=self._json_model,
85
+ contents=message,
86
+ config=types.GenerateContentConfig(
87
+ system_instruction=f"{self.instructions}.{additional_instructions}",
88
+ tools=[types.Tool(function_declarations=[function_definition])],
89
+ tool_config={
90
+ "function_calling_config": {
91
+ "mode": "ANY", # Force a function call
92
+ }
93
+ },
94
+ ),
95
+ )
96
+
97
+ # The response is now a ToolCall, not a JSON string
98
+ try:
99
+ # log(response.candidates[0].content.parts[0].function_call, _print=True)
100
+ tool_call = response.candidates[0].content.parts[0].function_call
101
+ if tool_call and tool_call.name == function["name"]:
102
+ return tool_call.args
103
+ else:
104
+ log(
105
+ "==== Model did not return a tool call or returned the wrong one. ===="
106
+ )
107
+ log(f"Response: {response.text}", _print=True)
108
+ return {}
109
+ except Exception as e:
110
+ log(f"==== Failed to parse ToolCall response: {e} ====")
111
+ return {}
112
+
113
+ def generate_text(self, message, additional_instructions=""):
114
+ response = self.client.models.generate_content(
115
+ model=self._text_model,
116
+ config=types.GenerateContentConfig(
117
+ system_instruction=f"{self.instructions}.{additional_instructions}",
118
+ ),
119
+ contents=message,
120
+ )
121
+
122
+ # log(results, _print=True)
123
+ # log("=================== END REPORT ===================", _print=True)
124
+ return response.text
125
+
126
+ def summarize_text(self, text, primer=""):
127
+ primer = primer or self.instructions
128
+ response = self.client.models.generate_content(
129
+ model=self._summary_model,
130
+ config=types.GenerateContentConfig(
131
+ system_instruction=f"{primer}",
132
+ ),
133
+ contents=text,
134
+ )
135
+ log(response)
136
+ try:
137
+ result = response.candidates[0].content.parts[0].text
138
+ except Exception as e:
139
+ log(f"{type(e)}:{e}\n\n Unable to generate content ====")
140
+ return None
141
+
142
+ return result
143
+
144
+ def generate_audio_text(
145
+ self, audio_file, prompt="Transcribe this audio clip", **kwargs
146
+ ):
147
+ myfile = self.client.files.upload(
148
+ file=io.BytesIO(audio_file),
149
+ config={
150
+ "mime_type": "audio/mp3",
151
+ "display_name": kwargs.get("display_name", "audio.mp3"),
152
+ },
153
+ )
154
+
155
+ response = self.client.models.generate_content(
156
+ model=self._stt_model,
157
+ contents=[
158
+ prompt,
159
+ myfile,
160
+ ],
161
+ )
162
+ return response.text
163
+
164
+ def generate_audio(self, prompt, voice=None):
165
+ voice = voice or random.choice(
166
+ [
167
+ "Zephyr",
168
+ "Puck",
169
+ "Charon",
170
+ "Kore",
171
+ "Fenrir",
172
+ "Leda",
173
+ "Orus",
174
+ "Aoede",
175
+ "Callirhoe",
176
+ "Autonoe",
177
+ "Enceladus",
178
+ "Iapetus",
179
+ "Umbriel",
180
+ "Algieba",
181
+ "Despina",
182
+ "Erinome",
183
+ "Algenib",
184
+ "Rasalgethi",
185
+ "Laomedeia",
186
+ "Achernar",
187
+ "Alnilam",
188
+ "Schedar",
189
+ "Gacrux",
190
+ "Pulcherrima",
191
+ "Achird",
192
+ "Zubenelgenubi",
193
+ "Vindemiatrix",
194
+ "Sadachbia",
195
+ "Sadaltager",
196
+ "Sulafar",
197
+ ]
198
+ )
199
+
200
+ try:
201
+ response = self.client.models.generate_content(
202
+ model=self._tts_model,
203
+ contents=prompt,
204
+ config=types.GenerateContentConfig(
205
+ response_modalities=["AUDIO"],
206
+ speech_config=types.SpeechConfig(
207
+ voice_config=types.VoiceConfig(
208
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(
209
+ voice_name=voice,
210
+ )
211
+ )
212
+ ),
213
+ ),
214
+ )
215
+ blob = response.candidates[0].content.parts[0].inline_data
216
+
217
+ # Create a WAV file in memory from the raw audio bytes
218
+ wav_buffer = self._create_wav_header(blob.data)
219
+
220
+ # 2. Load the WAV audio using pydub, which will now correctly read the header
221
+ audio_segment = AudioSegment.from_file(wav_buffer, format="wav")
222
+
223
+ # 3. Create a new in-memory buffer for the MP3 output
224
+ mp3_buffer = io.BytesIO()
225
+
226
+ # 4. Export the audio segment directly to the in-memory buffer
227
+ audio_segment.export(mp3_buffer, format="mp3")
228
+
229
+ # 5. Return the bytes from the buffer, not the filename
230
+ return mp3_buffer.getvalue()
231
+
232
+ except Exception as e:
233
+ log(
234
+ f"==== Error: Unable to generate audio ====\n{type(e)}:{e}", _print=True
235
+ )
236
+ # You can return a default empty byte string or re-raise the exception
237
+ raise e
238
+
239
+ def generate_image(self, prompt, **kwargs):
240
+ image = None
241
+ contents = [prompt]
242
+
243
+ if kwargs.get("files"):
244
+ for fn, f in kwargs.get("files").items():
245
+ media = io.BytesIO(f)
246
+ myfile = self.client.files.upload(
247
+ file=media, config={"mime_type": "image/webp", "display_name": fn}
248
+ )
249
+ contents += [myfile]
250
+
251
+ try:
252
+ # log(self._image_model, contents, _print=True)
253
+ response = self.client.models.generate_content(
254
+ model=self._image_model,
255
+ contents=contents,
256
+ config=types.GenerateContentConfig(
257
+ safety_settings=[
258
+ types.SafetySetting(
259
+ category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
260
+ threshold=types.HarmBlockThreshold.BLOCK_NONE,
261
+ ),
262
+ types.SafetySetting(
263
+ category=types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
264
+ threshold=types.HarmBlockThreshold.BLOCK_NONE,
265
+ ),
266
+ types.SafetySetting(
267
+ category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
268
+ threshold=types.HarmBlockThreshold.BLOCK_NONE,
269
+ ),
270
+ types.SafetySetting(
271
+ category=types.HarmCategory.HARM_CATEGORY_HARASSMENT,
272
+ threshold=types.HarmBlockThreshold.BLOCK_NONE,
273
+ ),
274
+ types.SafetySetting(
275
+ category=types.HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY,
276
+ threshold=types.HarmBlockThreshold.BLOCK_NONE,
277
+ ),
278
+ ],
279
+ image_config=types.ImageConfig(
280
+ aspect_ratio=kwargs.get("aspect_ratio", "3:4"),
281
+ image_size=kwargs.get("image_size", "2K"),
282
+ ),
283
+ ),
284
+ )
285
+ # log(response, _print=True)
286
+ # log(response.candidates[0], _print=True)
287
+ image_parts = [
288
+ part.inline_data.data
289
+ for part in response.candidates[0].content.parts
290
+ if part.inline_data
291
+ ]
292
+ image = image_parts[0]
293
+ except Exception as e:
294
+ log(
295
+ f"==== Error: Unable to create image ====\n\n{e}",
296
+ _print=True,
297
+ )
298
+ raise e
299
+ return image
@@ -0,0 +1,99 @@
1
+ import io
2
+ import json
3
+ import os
4
+ import random
5
+ import time
6
+ from base64 import b64decode
7
+
8
+ import openai
9
+ from ollama import ChatResponse, chat
10
+
11
+ from autonomous import log
12
+ from autonomous.model.autoattr import DictAttr, ListAttr, StringAttr
13
+ from autonomous.model.automodel import AutoModel
14
+
15
+
16
+ class LocalAIModel(AutoModel):
17
+ _client = None
18
+ instructions = StringAttr(
19
+ default="You are highly skilled AI trained to assist with various tasks."
20
+ )
21
+ description = StringAttr(
22
+ default="A helpful AI assistant trained to assist with various tasks."
23
+ )
24
+
25
+ @property
26
+ def client(self):
27
+ if not self._client:
28
+ self._client = "deepseek-r1" # OpenAI(api_key=os.environ.get("OPENAI_KEY"))
29
+ return self._client
30
+
31
+ def clear_agent(self):
32
+ pass
33
+
34
+ def clear_agents(self):
35
+ pass
36
+
37
+ # def _get_agent_id(self):
38
+ # pass
39
+
40
+ # def _add_function(self, user_function):
41
+ pass
42
+
43
+ def _format_messages(self, messages):
44
+ pass
45
+
46
+ def clear_files(self, file_id=None):
47
+ pass
48
+
49
+ def attach_file(self, file_contents, filename="dbdata.json"):
50
+ pass
51
+
52
+ def generate_json(self, messages, function, additional_instructions=""):
53
+ message = messages + additional_instructions
54
+ message += f"""
55
+ IMPORTANT: Respond in JSON FORMAT using the SCHEMA below. DO NOT add any text to the response outside of the supplied JSON schema:
56
+ {function}
57
+ """
58
+ response: ChatResponse = chat(
59
+ model=self.client,
60
+ messages=[
61
+ {
62
+ "role": "user",
63
+ "content": message,
64
+ },
65
+ ],
66
+ )
67
+ return response.message.content
68
+
69
+ def generate_text(self, messages, additional_instructions=""):
70
+ message = messages + additional_instructions
71
+ response: ChatResponse = chat(
72
+ model=self.client,
73
+ messages=[
74
+ {
75
+ "role": "user",
76
+ "content": message,
77
+ },
78
+ ],
79
+ )
80
+ return response.message.content
81
+
82
+ def generate_audio(self, prompt, **kwargs):
83
+ raise NotImplementedError
84
+
85
+ def generate_image(self, prompt, **kwargs):
86
+ raise NotImplementedError
87
+
88
+ def summarize_text(self, text, primer=""):
89
+ response: ChatResponse = chat(
90
+ model=self.client,
91
+ messages=[
92
+ {
93
+ "role": "system",
94
+ "content": f"You are a highly skilled AI trained in language comprehension and summarization.{primer}",
95
+ },
96
+ {"role": "user", "content": text},
97
+ ],
98
+ )
99
+ return response.message.content
@@ -16,7 +16,7 @@ from autonomous.model.automodel import AutoModel
16
16
 
17
17
  class OpenAIModel(AutoModel):
18
18
  _client = None
19
- _text_model = "gpt-4o-mini"
19
+ _text_model = "o3-mini"
20
20
  _image_model = "dall-e-3"
21
21
  _json_model = "gpt-4o"
22
22
  agent_id = StringAttr()
@@ -40,7 +40,10 @@ class OpenAIModel(AutoModel):
40
40
  def delete(self):
41
41
  self.clear_files()
42
42
  if self.agent_id:
43
- self.client.beta.assistants.delete(self.agent_id)
43
+ try:
44
+ self.client.beta.assistants.delete(self.agent_id)
45
+ except openai_NotFoundError:
46
+ log(f"==== Agent with ID: {self.agent_id} not found ====")
44
47
  return super().delete()
45
48
 
46
49
  def clear_agent(self):
@@ -51,15 +54,16 @@ class OpenAIModel(AutoModel):
51
54
 
52
55
  def clear_agents(self):
53
56
  assistants = self.client.beta.assistants.list().data
54
- log(assistants)
55
- for assistant in assistants:
56
- log(f"==== Deleting Agent with ID: {assistant.id} ====")
57
- try:
58
- self.client.beta.assistants.delete(assistant.id)
59
- except openai_NotFoundError:
60
- log(f"==== Agent with ID: {assistant.id} not found ====")
61
- self.agent_id = ""
62
- self.save()
57
+ if assistants:
58
+ log(assistants)
59
+ for assistant in assistants:
60
+ log(f"==== Deleting Agent with ID: {assistant.id} ====")
61
+ try:
62
+ self.client.beta.assistants.delete(assistant.id)
63
+ except openai_NotFoundError:
64
+ log(f"==== Agent with ID: {assistant.id} not found ====")
65
+ self.agent_id = ""
66
+ self.save()
63
67
 
64
68
  def _get_agent_id(self):
65
69
  try:
@@ -79,9 +83,9 @@ class OpenAIModel(AutoModel):
79
83
 
80
84
  def clear_files(self, file_id=None):
81
85
  if not file_id:
82
- for vs in self.client.beta.vector_stores.list().data:
86
+ for vs in self.client.vector_stores.list().data:
83
87
  try:
84
- self.client.beta.vector_stores.delete(vs.id)
88
+ self.client.vector_stores.delete(vs.id)
85
89
  except openai_NotFoundError:
86
90
  log(f"==== Vector Store {vs.id} not found ====")
87
91
  for sf in self.client.files.list().data:
@@ -97,8 +101,8 @@ class OpenAIModel(AutoModel):
97
101
  self.tools["file_search"] = {"type": "file_search"}
98
102
  # Create a vector store
99
103
  try:
100
- if vs := self.client.beta.vector_stores.list().data:
101
- self.vector_store = self.client.beta.vector_stores.retrieve(
104
+ if vs := self.client.vector_stores.list().data:
105
+ self.vector_store = self.client.vector_stores.retrieve(
102
106
  vector_store_id=vs[0].id
103
107
  ).id
104
108
  else:
@@ -106,17 +110,17 @@ class OpenAIModel(AutoModel):
106
110
  self.client.files.delete(file_id=sf.id)
107
111
  raise FileNotFoundError("No vector store found")
108
112
  except FileNotFoundError:
109
- self.vector_store = self.client.beta.vector_stores.create(
113
+ self.vector_store = self.client.vector_stores.create(
110
114
  name="World Reference",
111
115
  expires_after={"anchor": "last_active_at", "days": 14},
112
116
  ).id
113
- log(f"==== Vector Store ID: {self.vector_store}====")
117
+ log(f"==== Vector Store ID: {self.vector_store}====", _print=True)
114
118
  # Attach File
115
119
  file_obj = self.client.files.create(
116
120
  file=(filename, file_contents), purpose="assistants"
117
121
  )
118
- log(f"==== FileStore ID: {file_obj.id}====")
119
- self.client.beta.vector_stores.files.create(
122
+ log(f"==== FileStore ID: {file_obj.id}====", _print=True)
123
+ self.client.vector_stores.files.create(
120
124
  vector_store_id=self.vector_store,
121
125
  file_id=file_obj.id,
122
126
  )
@@ -198,14 +202,14 @@ IMPORTANT: Always use the function 'response' tool to respond to the user with o
198
202
  ]:
199
203
  running_job = False
200
204
 
201
- except openai.BadRequestError as e:
205
+ except openai.BadRequestError as err:
202
206
  # Handle specific bad request errors
203
- error_message = e.json_body.get("error", {}).get("message", "")
204
- if "already has an active run" in error_message:
207
+ log(f"==== Error: {err} ====", _print=True)
208
+ if "already has an active run" in str(err):
205
209
  log("Previous run is still active. Waiting...", _print=True)
206
210
  time.sleep(2) # wait before retrying or checking run status
207
211
  else:
208
- raise e
212
+ raise err
209
213
 
210
214
  # while run.status in ["queued", "in_progress"]:
211
215
  # run = self.client.beta.threads.runs.retrieve(
@@ -217,7 +221,7 @@ IMPORTANT: Always use the function 'response' tool to respond to the user with o
217
221
  log(f"==== !!! ERROR !!!: {run.last_error} ====", _print=True)
218
222
  return None
219
223
  log("=================== RUN COMPLETED ===================", _print=True)
220
- log(run.status, _print=True)
224
+ # log(run.status, _print=True)
221
225
  if run.status == "completed":
222
226
  response = self.client.beta.threads.messages.list(thread_id=thread.id)
223
227
  results = response.data[0].content[0].text.value
@@ -236,8 +240,8 @@ IMPORTANT: Always use the function 'response' tool to respond to the user with o
236
240
  log(f"==== Invalid JSON:\n{results}", _print=True)
237
241
  return {}
238
242
  else:
239
- log(f"==== Results: {results}", _print=True)
240
- log("=================== END REPORT ===================", _print=True)
243
+ # log(f"==== Results: {results}", _print=True)
244
+ # log("=================== END REPORT ===================", _print=True)
241
245
  return results
242
246
 
243
247
  def generate_text(self, messages, additional_instructions=""):
@@ -278,16 +282,34 @@ IMPORTANT: Always use the function 'response' tool to respond to the user with o
278
282
 
279
283
  def generate_audio(self, prompt, **kwargs):
280
284
  voice = kwargs.get("voice") or random.choice(
281
- ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
285
+ [
286
+ "alloy",
287
+ "ash",
288
+ "ballad",
289
+ "coral",
290
+ "echo",
291
+ "fable",
292
+ "onyx",
293
+ "nova",
294
+ "sage",
295
+ "shimmer",
296
+ ]
282
297
  )
283
298
  response = self.client.audio.speech.create(
284
299
  model="tts-1",
285
300
  voice=voice,
286
301
  input=prompt,
287
302
  )
288
- log(response, _print=True)
303
+ # log(response, _print=True)
289
304
  return response.read()
290
305
 
306
+ def generate_audio_text(self, audio_file, **kwargs):
307
+ response = self.client.audio.transcriptions.create(
308
+ model="gpt-4o-transcribe", file=audio_file, language="en", **kwargs
309
+ )
310
+ log(response, _print=True)
311
+ return response.text
312
+
291
313
  def generate_image(self, prompt, **kwargs):
292
314
  image = None
293
315
  try:
@@ -299,7 +321,8 @@ IMPORTANT: Always use the function 'response' tool to respond to the user with o
299
321
  )
300
322
  image_dict = response.data[0]
301
323
  except Exception as e:
302
- print(f"==== Error: Unable to create image ====\n\n{e}")
324
+ log(f"==== Error: Unable to create image ====\n\n{e}", _print=True)
325
+ raise e
303
326
  else:
304
327
  image = b64decode(image_dict.b64_json)
305
328
  return image
@@ -14,12 +14,6 @@ class TextAgent(BaseAgent):
14
14
  default="A helpful AI assistant trained to assist with generating text according to the given requirements."
15
15
  )
16
16
 
17
- def clear_files(self, file_id=None):
18
- return self.get_client().clear_files(file_id)
19
-
20
- def attach_file(self, file_contents, filename="dbdata.json"):
21
- return self.get_client().attach_file(file_contents, filename)
22
-
23
17
  def summarize_text(self, text, primer=""):
24
18
  return self.get_client().summarize_text(text, primer)
25
19
 
autonomous/db/fields.py CHANGED
@@ -721,8 +721,7 @@ class EmbeddedDocumentField(BaseField):
721
721
  or issubclass(document_type, EmbeddedDocument)
722
722
  ):
723
723
  self.error(
724
- "Invalid embedded document class provided to an "
725
- "EmbeddedDocumentField"
724
+ "Invalid embedded document class provided to an EmbeddedDocumentField"
726
725
  )
727
726
 
728
727
  self.document_type_obj = document_type
@@ -1494,7 +1493,7 @@ class GenericReferenceField(BaseField):
1494
1493
  get_document(value.get("_cls")), value.get("_ref")
1495
1494
  )
1496
1495
  except DoesNotExist:
1497
- log(f"{value} DoesNotExist")
1496
+ # log(f"{value} DoesNotExist")
1498
1497
  return
1499
1498
 
1500
1499
  if isinstance(value, Document):
@@ -117,10 +117,7 @@ class BaseQuerySet:
117
117
  if q_obj:
118
118
  # Make sure proper query object is passed.
119
119
  if not isinstance(q_obj, QNode):
120
- msg = (
121
- "Not a query object: %s. "
122
- "Did you intend to use key=value?" % q_obj
123
- )
120
+ msg = "Not a query object: %s. Did you intend to use key=value?" % q_obj
124
121
  raise InvalidQueryError(msg)
125
122
  query &= q_obj
126
123
 
@@ -1,3 +1,4 @@
1
+ from autonomous import log
1
2
  from autonomous.db.errors import OperationError
2
3
  from autonomous.db.queryset.base import (
3
4
  CASCADE,