amrita_core 0.1.0__py3-none-any.whl → 0.2.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.
amrita_core/__init__.py CHANGED
@@ -13,7 +13,7 @@ from .libchat import (
13
13
  from .logging import debug_log, logger
14
14
  from .preset import PresetManager, PresetReport
15
15
  from .tools import mcp
16
- from .tools.manager import ToolsManager, on_tools
16
+ from .tools.manager import ToolsManager, on_tools, simple_tool
17
17
  from .tools.models import (
18
18
  FunctionDefinitionSchema,
19
19
  FunctionParametersSchema,
@@ -73,6 +73,7 @@ __all__ = [
73
73
  "on_precompletion",
74
74
  "on_tools",
75
75
  "set_config",
76
+ "simple_tool",
76
77
  "text_generator",
77
78
  "tools_caller",
78
79
  ]
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
2
4
  import copy
3
5
  from abc import ABC, abstractmethod
@@ -434,10 +436,9 @@ class ChatObject:
434
436
  _is_running: bool = False # Whether it is running
435
437
  _is_done: bool = False # Whether it has completed
436
438
  _task: Task[None]
437
- _has_task: bool = False
438
439
  _err: BaseException | None = None
439
- _wait: bool = True
440
440
  _queue_done: bool = False
441
+ _has_consumer: bool = False
441
442
  __done_marker = object()
442
443
 
443
444
  def __init__(
@@ -446,7 +447,6 @@ class ChatObject:
446
447
  user_input: USER_INPUT,
447
448
  context: Memory,
448
449
  session_id: str,
449
- run_blocking: bool = True,
450
450
  queue_size: int = 25,
451
451
  overflow_queue_size: int = 45,
452
452
  ) -> None:
@@ -468,7 +468,6 @@ class ChatObject:
468
468
  self.time = datetime.now(utc)
469
469
  self.config: AmritaConfig = get_config()
470
470
  self.last_call = datetime.now(utc)
471
- self._wait = run_blocking
472
471
 
473
472
  # Initialize async queue for streaming responses
474
473
  self.response_queue = asyncio.Queue(queue_size)
@@ -484,15 +483,6 @@ class ChatObject:
484
483
  """
485
484
  return self._err
486
485
 
487
- def call(self):
488
- """
489
- Get callable object
490
-
491
- Returns:
492
- Callable object (usually the class's __call__ method)
493
- """
494
- return self.__call__()
495
-
496
486
  def is_running(self) -> bool:
497
487
  """
498
488
  Check if the task is running
@@ -518,14 +508,31 @@ class ChatObject:
518
508
  """
519
509
  self._is_done = True
520
510
  self._is_running = False
521
- self._task.cancel()
511
+ if hasattr(self, "_task") and not self._task.done():
512
+ self._task.cancel()
522
513
 
523
514
  def __await__(self):
524
515
  if not hasattr(self, "_task"):
525
516
  raise RuntimeError("ChatObject not running")
526
517
  return self._task.__await__()
527
518
 
528
- async def __call__(self) -> None:
519
+ async def __aenter__(self) -> Self:
520
+ if not hasattr(self, "_task"):
521
+ raise RuntimeError("ChatObject not running")
522
+ if self._has_consumer:
523
+ raise RuntimeError("ChatObject already has a consumer")
524
+ return self
525
+
526
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
527
+ self.terminate()
528
+
529
+ def begin(self) -> Self:
530
+ if not hasattr(self, "_task"):
531
+ logger.debug("Starting chat object task...")
532
+ self._task = asyncio.create_task(self._entry())
533
+ return self
534
+
535
+ async def _entry(self) -> None:
529
536
  """Call chat object to process messages
530
537
 
531
538
  Args:
@@ -533,11 +540,6 @@ class ChatObject:
533
540
  matcher: Matcher
534
541
  bot: Bot instance
535
542
  """
536
- if not self._has_task:
537
- logger.debug("Starting chat object task...")
538
- self._has_task = True
539
- self._task = asyncio.create_task(self.__call__())
540
- return await self._task if self._wait else None
541
543
  if not self._is_running and not self._is_done:
542
544
  self.stream_id = uuid4().hex
543
545
  logger.debug(f"Starting chat processing, stream ID:{self.stream_id}")
@@ -553,6 +555,7 @@ class ChatObject:
553
555
  self._is_done = True
554
556
  self.end_at = datetime.now(utc)
555
557
  chat_manager.running_chat_object_id2map.pop(self.stream_id, None)
558
+ chat_manager.clean_obj(self.session_id, 1000) # To avoid memory leaks
556
559
  logger.debug("Chat event processing completed")
557
560
 
558
561
  else:
@@ -686,6 +689,9 @@ class ChatObject:
686
689
  Yields:
687
690
  Either a string or MessageContent object from the response queue
688
691
  """
692
+ if self._has_consumer:
693
+ raise RuntimeError("Queue is already being consumed.")
694
+ self._has_consumer = True
689
695
  return self._response_generator()
690
696
 
691
697
  async def full_response(self) -> str:
@@ -1,10 +1,22 @@
1
+ import inspect
2
+ import re
1
3
  import typing
4
+ from asyncio import iscoroutinefunction
2
5
  from collections.abc import Awaitable, Callable
3
- from typing import Any, ClassVar, overload
6
+ from functools import wraps
7
+ from typing import Any, ClassVar, get_args, get_origin, get_type_hints, overload
4
8
 
5
9
  from typing_extensions import Self
6
10
 
7
- from .models import FunctionDefinitionSchema, ToolContext, ToolData, ToolFunctionSchema
11
+ from .models import (
12
+ JSON_OBJECT_TYPE,
13
+ FunctionDefinitionSchema,
14
+ FunctionParametersSchema,
15
+ FunctionPropertySchema,
16
+ ToolContext,
17
+ ToolData,
18
+ ToolFunctionSchema,
19
+ )
8
20
 
9
21
  T = typing.TypeVar("T")
10
22
 
@@ -128,6 +140,199 @@ class ToolsManager:
128
140
  return list(self._disabled_tools)
129
141
 
130
142
 
143
+ def _parse_google_docstring(docstring: str | None) -> tuple[str, dict[str, str]]:
144
+ """
145
+ Parse Google-style docstring to extract function description and parameter descriptions.
146
+
147
+ Args:
148
+ docstring: The docstring to parse
149
+
150
+ Returns:
151
+ A tuple containing (function_description, parameter_descriptions_dict)
152
+ """
153
+ if not docstring:
154
+ return "(no description provided for this tool)", {}
155
+
156
+ # Clean up the docstring
157
+ lines = [line.strip() for line in docstring.split("\n") if line.strip()]
158
+
159
+ # Find the index where Args section starts
160
+ args_start_idx = -1
161
+ for i, line in enumerate(lines):
162
+ if line.lower().startswith("args:"):
163
+ args_start_idx = i
164
+ break
165
+
166
+ # Extract function description (everything before Args section)
167
+ if args_start_idx != -1:
168
+ func_desc_lines = lines[:args_start_idx]
169
+ func_desc = " ".join(func_desc_lines).strip()
170
+
171
+ # Extract Args section
172
+ args_lines = lines[args_start_idx + 1 :]
173
+ else:
174
+ # No Args section found
175
+ func_desc = " ".join(lines).strip()
176
+ args_lines = []
177
+
178
+ # Process Args section
179
+ param_descriptions = {}
180
+
181
+ # Pattern to match parameter descriptions in the format:
182
+ # param_name (type): description
183
+ # or
184
+ # param_name: description
185
+ param_pattern = r"^([a-zA-Z_][a-zA-Z0-9_]*)\s*(?:\(([^)]+)\))?\s*:\s*(.*)"
186
+
187
+ for line in args_lines:
188
+ # Look for parameter pattern at the beginning of the line or after whitespace
189
+ match = re.match(param_pattern, line)
190
+ if match:
191
+ param_name = match.group(1)
192
+ # param_type = match.group(2) # We don't need the type since it's in the annotation
193
+ param_desc = match.group(3).strip()
194
+
195
+ if param_desc:
196
+ param_descriptions[param_name] = param_desc
197
+ else:
198
+ param_descriptions[param_name] = f"Parameter {param_name}"
199
+
200
+ if not func_desc:
201
+ func_desc = "(no description provided for this tool)"
202
+
203
+ return func_desc, param_descriptions
204
+
205
+
206
+ def simple_tool(func: Callable[..., Any | Awaitable[Any]]):
207
+ """
208
+ A decorator that creates a ToolData object based on the function signature and annotations.
209
+ It automatically generates parameter descriptions and metadata.
210
+
211
+ Here is an example of how to use this decorator:
212
+
213
+ ```python
214
+ @simple_tool
215
+ def add(a: int, b: int) -> int:
216
+ \"""Add two numbers together.
217
+
218
+ Args:
219
+ a (int): The first number.
220
+ b (int): The second number.
221
+
222
+ Returns:
223
+ int: The sum of the two numbers.
224
+ \"""
225
+ return a + b
226
+ ```
227
+ """
228
+ signature: inspect.Signature = inspect.signature(func)
229
+
230
+ # Parse Google-style docstring to get function and parameter descriptions
231
+ func_desc, param_descriptions = _parse_google_docstring(func.__doc__)
232
+
233
+ # Get the type hints for the function
234
+ type_hints: dict[str, Any] = get_type_hints(
235
+ func, globalns=globals(), localns=locals()
236
+ )
237
+
238
+ # Prepare parameters schema
239
+ properties = {}
240
+ required = []
241
+
242
+ for param_name, param in signature.parameters.items():
243
+ # Skip 'self' parameter for methods
244
+ if param_name == "self":
245
+ continue
246
+
247
+ # Determine parameter type from type hints
248
+ param_type = type_hints.get(param_name)
249
+
250
+ # If parameter has no type hint, default to string
251
+ json_type: JSON_OBJECT_TYPE = "string"
252
+ if param_type:
253
+ # Map Python types to JSON schema types
254
+ if hasattr(param_type, "__origin__"):
255
+ origin = get_origin(param_type)
256
+ if origin is not None:
257
+ if issubclass(origin, list):
258
+ json_type = "array"
259
+ elif issubclass(origin, dict):
260
+ json_type = "object"
261
+ elif origin is typing.Union:
262
+ args = get_args(param_type)
263
+ if type(None) in args:
264
+ # Handle Optional types - don't add to required
265
+ pass
266
+ else:
267
+ # For Union types, use the first non-None type if available
268
+ non_none_types = [
269
+ arg for arg in args if arg is not type(None)
270
+ ]
271
+ if non_none_types:
272
+ param_type = non_none_types[0]
273
+ json_type = _python_type_to_json_type(param_type)
274
+ else:
275
+ json_type = _python_type_to_json_type(param_type)
276
+ else:
277
+ json_type = _python_type_to_json_type(param_type)
278
+
279
+ # Get parameter description from parsed docstring if available
280
+ param_desc = param_descriptions.get(param_name, f"Parameter {param_name}")
281
+
282
+ # Check if parameter is required (no default value)
283
+ is_required = param.default == inspect.Parameter.empty
284
+ if is_required:
285
+ required.append(param_name)
286
+ property_schema = FunctionPropertySchema(type=json_type, description=param_desc)
287
+
288
+ properties[param_name] = property_schema
289
+
290
+ parameters_schema = FunctionParametersSchema(
291
+ type="object", properties=properties if properties else None, required=required
292
+ )
293
+
294
+ function_def = FunctionDefinitionSchema(
295
+ name=func.__name__, description=func_desc, parameters=parameters_schema
296
+ )
297
+
298
+ # Create a wrapper function that accepts a dictionary of parameters
299
+ @on_tools(function_def, strict=True)
300
+ @wraps(func)
301
+ async def tool_wrapper(params: dict[str, Any]) -> str:
302
+ bound_args: inspect.BoundArguments = signature.bind(**params)
303
+ bound_args.apply_defaults()
304
+
305
+ result = (
306
+ await func(**bound_args.arguments)
307
+ if iscoroutinefunction(func)
308
+ else func(**bound_args.arguments)
309
+ )
310
+
311
+ # Convert result to string as expected by the schema
312
+ return str(result)
313
+
314
+ return tool_wrapper
315
+
316
+
317
+ def _python_type_to_json_type(python_type: type[Any]) -> JSON_OBJECT_TYPE:
318
+ """Convert Python type to JSON schema type."""
319
+ if python_type is str:
320
+ return "string"
321
+ elif python_type is int:
322
+ return "integer"
323
+ elif python_type is float:
324
+ return "number"
325
+ elif python_type is bool:
326
+ return "boolean"
327
+ elif python_type is list:
328
+ return "array"
329
+ elif python_type is dict:
330
+ return "object"
331
+ else:
332
+ # Default to string for unrecognized types
333
+ return "string"
334
+
335
+
131
336
  def on_tools(
132
337
  data: FunctionDefinitionSchema,
133
338
  custom_run: bool = False,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amrita_core
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Agent core of Project Amrita
5
5
  Project-URL: Homepage, https://github.com/AmritaBot/AmritaCore
6
6
  Project-URL: Source, https://github.com/AmritaBot/AmritaCore
@@ -60,6 +60,8 @@ This repository contains documentation organized as follows:
60
60
 
61
61
  Documentation is currently under construction. For quick start, please refer to the examples in the `demo/` folder.
62
62
 
63
+ Please view [Docs](https://amrita-core.suggar.top) for more information.
64
+
63
65
  ## 🛠️ Quick Start
64
66
 
65
67
  To quickly start using AmritaCore, check out the examples in the [demo](./demo/) directory. The basic example demonstrates how to initialize the core, configure settings, and run a simple chat session with the AI assistant.
@@ -1,5 +1,5 @@
1
- amrita_core/__init__.py,sha256=uNev8VnIbKg6CcdsaB_H7DgTLTNXLyTRoOqlAOYaBu8,2329
2
- amrita_core/chatmanager.py,sha256=PV_IfEBiWfjIjKykaJ4XtcEHFYKCvMrwDy2eHVVy5kQ,32436
1
+ amrita_core/__init__.py,sha256=C-dVBkL3Zb03KpkIaJtDlInTMfhBWVA9-Nn0WrFk61Q,2361
2
+ amrita_core/chatmanager.py,sha256=3l48yc_LBWvlQfA-5Tu4CIKo3Q34ukOczERXcjE7Nag,32780
3
3
  amrita_core/config.py,sha256=T8SAH5-ND496nMEQ-b-VGnmDdaGixlDVCSLZ985f860,5377
4
4
  amrita_core/libchat.py,sha256=bM-WhD6BGvn_aFHAkywYGaU6vF67QAfudsHfB2Ia-M4,5614
5
5
  amrita_core/logging.py,sha256=aUPGGMntZq-r5i60DAvTXfMaeD5ac93Nxus8rcIWw_8,1724
@@ -16,11 +16,11 @@ amrita_core/hook/event.py,sha256=GoyV4M93BE8MwGcp_nJwduyib3DW1NTNe31RfWSGUQw,224
16
16
  amrita_core/hook/exception.py,sha256=Q4U8Vd8fZursEnuCwDyFjJizJGW75coj_0Gik4jERoM,226
17
17
  amrita_core/hook/matcher.py,sha256=NGU6ClyIaNMWMdFduZkLTXm1st2YpgVFJIyn45Qu31M,7666
18
18
  amrita_core/hook/on.py,sha256=C9Z66Nliy6Rq4xyn4CZt3V8NXWPEHPuavp8tDBXcP0w,451
19
- amrita_core/tools/manager.py,sha256=SwFjopuTa2OqV6bEXRtRaP81d9T3nLiQI5sHvge5NO4,5229
19
+ amrita_core/tools/manager.py,sha256=EllKJMpok6uTe3pcq5UziPSfambk8xsUQWJBl7iqePE,12094
20
20
  amrita_core/tools/mcp.py,sha256=K-6SjsD-KUfRgII7HuX2AADClD79qwy2DyxXbSpjyYk,12739
21
21
  amrita_core/tools/models.py,sha256=lUpdYSOh-rzOM1jIJURBYgqaZGHWOznnGGyGJExGFeI,12246
22
- amrita_core-0.1.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
23
- amrita_core-0.1.0.dist-info/METADATA,sha256=AFHnyDC6XTdxH4LJf3toCJQzfkTsP2Brs5oFXAwZuSA,4125
24
- amrita_core-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
25
- amrita_core-0.1.0.dist-info/top_level.txt,sha256=7mB9cYXC_VZ73UXo6DXplcfSvMGeP1sTNHza46x2FTk,12
26
- amrita_core-0.1.0.dist-info/RECORD,,
22
+ amrita_core-0.2.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
23
+ amrita_core-0.2.0.dist-info/METADATA,sha256=KOPOEh1k-vtf76fWfnusb0dwLFnejziJtzVqcWFL9Ws,4199
24
+ amrita_core-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
25
+ amrita_core-0.2.0.dist-info/top_level.txt,sha256=7mB9cYXC_VZ73UXo6DXplcfSvMGeP1sTNHza46x2FTk,12
26
+ amrita_core-0.2.0.dist-info/RECORD,,