tinyagent-py 0.0.4__py3-none-any.whl → 0.0.5__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.
tinyagent/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .tiny_agent import TinyAgent,tool
2
+ from .mcp_client import MCPClient
3
+
4
+ __all__ = ["TinyAgent", "MCPClient","tool"]
@@ -0,0 +1,167 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ from typing import Dict, List, Optional, Any, Tuple, Callable
5
+
6
+ # Keep your MCPClient implementation unchanged
7
+ import asyncio
8
+ from contextlib import AsyncExitStack
9
+
10
+ # MCP core imports
11
+ from mcp import ClientSession, StdioServerParameters
12
+ from mcp.client.stdio import stdio_client
13
+
14
+ # Set up logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class MCPClient:
18
+ def __init__(self, logger: Optional[logging.Logger] = None):
19
+ self.session = None
20
+ self.exit_stack = AsyncExitStack()
21
+ self.logger = logger or logging.getLogger(__name__)
22
+
23
+ # Simplified callback system
24
+ self.callbacks: List[callable] = []
25
+
26
+ self.logger.debug("MCPClient initialized")
27
+
28
+ def add_callback(self, callback: callable) -> None:
29
+ """
30
+ Add a callback function to the client.
31
+
32
+ Args:
33
+ callback: A function that accepts (event_name, client, **kwargs)
34
+ """
35
+ self.callbacks.append(callback)
36
+
37
+ async def _run_callbacks(self, event_name: str, **kwargs) -> None:
38
+ """
39
+ Run all registered callbacks for an event.
40
+
41
+ Args:
42
+ event_name: The name of the event
43
+ **kwargs: Additional data for the event
44
+ """
45
+ for callback in self.callbacks:
46
+ try:
47
+ logger.debug(f"Running callback: {callback}")
48
+ if asyncio.iscoroutinefunction(callback):
49
+ logger.debug(f"Callback is a coroutine function")
50
+ await callback(event_name, self, **kwargs)
51
+ else:
52
+ # Check if the callback is a class with an async __call__ method
53
+ if hasattr(callback, '__call__') and asyncio.iscoroutinefunction(callback.__call__):
54
+ logger.debug(f"Callback is a class with an async __call__ method")
55
+ await callback(event_name, self, **kwargs)
56
+ else:
57
+ logger.debug(f"Callback is a regular function")
58
+ callback(event_name, self, **kwargs)
59
+ except Exception as e:
60
+ logger.error(f"Error in callback for {event_name}: {str(e)}")
61
+
62
+ async def connect(self, command: str, args: list[str]):
63
+ """
64
+ Launches the MCP server subprocess and initializes the client session.
65
+ :param command: e.g. "python" or "node"
66
+ :param args: list of args to pass, e.g. ["my_server.py"] or ["build/index.js"]
67
+ """
68
+ # Prepare stdio transport parameters
69
+ params = StdioServerParameters(command=command, args=args)
70
+ # Open the stdio client transport
71
+ self.stdio, self.sock_write = await self.exit_stack.enter_async_context(
72
+ stdio_client(params)
73
+ )
74
+ # Create and initialize the MCP client session
75
+ self.session = await self.exit_stack.enter_async_context(
76
+ ClientSession(self.stdio, self.sock_write)
77
+ )
78
+ await self.session.initialize()
79
+
80
+ async def list_tools(self):
81
+ resp = await self.session.list_tools()
82
+ print("Available tools:")
83
+ for tool in resp.tools:
84
+ print(f" • {tool.name}: {tool.description}")
85
+
86
+ async def call_tool(self, name: str, arguments: dict):
87
+ """
88
+ Invokes a named tool and returns its raw content list.
89
+ """
90
+ # Notify tool start
91
+ await self._run_callbacks("tool_start", tool_name=name, arguments=arguments)
92
+
93
+ try:
94
+ resp = await self.session.call_tool(name, arguments)
95
+
96
+ # Notify tool end
97
+ await self._run_callbacks("tool_end", tool_name=name, arguments=arguments,
98
+ result=resp.content, success=True)
99
+
100
+ return resp.content
101
+ except Exception as e:
102
+ # Notify tool end with error
103
+ await self._run_callbacks("tool_end", tool_name=name, arguments=arguments,
104
+ error=str(e), success=False)
105
+ raise
106
+
107
+ async def close(self):
108
+ """Clean up subprocess and streams."""
109
+ if self.session:
110
+ try:
111
+ await self.session.close()
112
+ except Exception as e:
113
+ self.logger.error(f"Error closing session: {e}")
114
+ if self.exit_stack:
115
+ try:
116
+ await self.exit_stack.aclose()
117
+ except (RuntimeError, asyncio.CancelledError) as e:
118
+ # Log the error but don't re-raise it
119
+ self.logger.error(f"Error during client cleanup: {e}")
120
+ finally:
121
+ # Always reset these regardless of success or failure
122
+ self.session = None
123
+ self.exit_stack = AsyncExitStack()
124
+
125
+ async def run_example():
126
+ """Example usage of MCPClient with proper logging."""
127
+ import sys
128
+ from tinyagent.hooks.logging_manager import LoggingManager
129
+
130
+ # Create and configure logging manager
131
+ log_manager = LoggingManager(default_level=logging.INFO)
132
+ log_manager.set_levels({
133
+ 'tinyagent.mcp_client': logging.DEBUG, # Debug for this module
134
+ 'tinyagent.tiny_agent': logging.INFO,
135
+ })
136
+
137
+ # Configure a console handler
138
+ console_handler = logging.StreamHandler(sys.stdout)
139
+ log_manager.configure_handler(
140
+ console_handler,
141
+ format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
142
+ level=logging.DEBUG
143
+ )
144
+
145
+ # Get module-specific logger
146
+ mcp_logger = log_manager.get_logger('tinyagent.mcp_client')
147
+
148
+ mcp_logger.debug("Starting MCPClient example")
149
+
150
+ # Create client with our logger
151
+ client = MCPClient(logger=mcp_logger)
152
+
153
+ try:
154
+ # Connect to a simple echo server
155
+ await client.connect("python", ["-m", "mcp.examples.echo_server"])
156
+
157
+ # List available tools
158
+ await client.list_tools()
159
+
160
+ # Call the echo tool
161
+ result = await client.call_tool("echo", {"message": "Hello, MCP!"})
162
+ mcp_logger.info(f"Echo result: {result}")
163
+
164
+ finally:
165
+ # Clean up
166
+ await client.close()
167
+ mcp_logger.debug("Example completed")
@@ -0,0 +1,958 @@
1
+ # Import LiteLLM for model interaction
2
+ import litellm
3
+ import json
4
+ import logging
5
+ from typing import Dict, List, Optional, Any, Tuple, Callable, Union, Type, get_type_hints
6
+ from .mcp_client import MCPClient
7
+ import asyncio
8
+ import tiktoken # Add tiktoken import for token counting
9
+ import inspect
10
+ import functools
11
+ import uuid
12
+ from .storage import Storage # ← your abstract base
13
+ import traceback
14
+ import time # Add time import for Unix timestamps
15
+ # Module-level logger; configuration is handled externally.
16
+ logger = logging.getLogger(__name__)
17
+ #litellm.callbacks = ["arize_phoenix"]
18
+
19
+ def tool(name: Optional[str] = None, description: Optional[str] = None,
20
+ schema: Optional[Dict[str, Any]] = None):
21
+ """
22
+ Decorator to convert a Python function or class into a tool for TinyAgent.
23
+
24
+ Args:
25
+ name: Optional custom name for the tool (defaults to function/class name)
26
+ description: Optional description (defaults to function/class docstring)
27
+ schema: Optional JSON schema for the tool parameters (auto-generated if not provided)
28
+
29
+ Returns:
30
+ Decorated function or class with tool metadata
31
+ """
32
+ def decorator(func_or_class):
33
+ # Determine if we're decorating a function or class
34
+ is_class = inspect.isclass(func_or_class)
35
+
36
+ # Get the name (use provided name or function/class name)
37
+ tool_name = name or func_or_class.__name__
38
+
39
+ # Get the description (use provided description or docstring)
40
+ tool_description = description or inspect.getdoc(func_or_class) or f"Tool based on {tool_name}"
41
+
42
+ # Generate schema if not provided
43
+ tool_schema = schema or {}
44
+ if not tool_schema:
45
+ if is_class:
46
+ # For classes, look at the __init__ method
47
+ init_method = func_or_class.__init__
48
+ tool_schema = _generate_schema_from_function(init_method)
49
+ else:
50
+ # For functions, use the function itself
51
+ tool_schema = _generate_schema_from_function(func_or_class)
52
+
53
+ # Attach metadata to the function or class
54
+ func_or_class._tool_metadata = {
55
+ "name": tool_name,
56
+ "description": tool_description,
57
+ "schema": tool_schema,
58
+ "is_class": is_class
59
+ }
60
+
61
+ return func_or_class
62
+
63
+ return decorator
64
+
65
+ def _generate_schema_from_function(func: Callable) -> Dict[str, Any]:
66
+ """
67
+ Generate a JSON schema for a function based on its signature and type hints.
68
+
69
+ Args:
70
+ func: The function to analyze
71
+
72
+ Returns:
73
+ A JSON schema object for the function parameters
74
+ """
75
+ # Get function signature and type hints
76
+ sig = inspect.signature(func)
77
+ type_hints = get_type_hints(func)
78
+
79
+ # Skip 'self' parameter for methods
80
+ params = {
81
+ name: param for name, param in sig.parameters.items()
82
+ if name != 'self' and name != 'cls'
83
+ }
84
+
85
+ # Build properties dictionary
86
+ properties = {}
87
+ required = []
88
+
89
+ for name, param in params.items():
90
+ # Get parameter type
91
+ param_type = type_hints.get(name, Any)
92
+
93
+ # Create property schema
94
+ prop_schema = {"description": ""}
95
+
96
+ # Map Python types to JSON schema types
97
+ if param_type == str:
98
+ prop_schema["type"] = "string"
99
+ elif param_type == int:
100
+ prop_schema["type"] = "integer"
101
+ elif param_type == float:
102
+ prop_schema["type"] = "number"
103
+ elif param_type == bool:
104
+ prop_schema["type"] = "boolean"
105
+ elif param_type == list or param_type == List:
106
+ prop_schema["type"] = "array"
107
+ elif param_type == dict or param_type == Dict:
108
+ prop_schema["type"] = "object"
109
+ else:
110
+ prop_schema["type"] = "string" # Default to string for complex types
111
+
112
+ properties[name] = prop_schema
113
+
114
+ # Check if parameter is required
115
+ if param.default == inspect.Parameter.empty:
116
+ required.append(name)
117
+
118
+ # Build the final schema
119
+ schema = {
120
+ "type": "object",
121
+ "properties": properties
122
+ }
123
+
124
+ if required:
125
+ schema["required"] = required
126
+
127
+ return schema
128
+
129
+ DEFAULT_SYSTEM_PROMPT = (
130
+ "You are a helpful AI assistant with access to a variety of tools. "
131
+ "Use the tools when appropriate to accomplish tasks. "
132
+ "If a tool you need isn't available, just say so."
133
+ )
134
+
135
+ class TinyAgent:
136
+ """
137
+ A minimal implementation of an agent powered by MCP and LiteLLM,
138
+ now with session/state persistence.
139
+ """
140
+ session_state: Dict[str, Any] = {}
141
+ user_id: Optional[str] = None
142
+ session_id: Optional[str] = None
143
+
144
+ def __init__(
145
+ self,
146
+ model: str = "gpt-4.1-mini",
147
+ api_key: Optional[str] = None,
148
+ system_prompt: Optional[str] = None,
149
+ temperature: float = 0.0,
150
+ logger: Optional[logging.Logger] = None,
151
+ model_kwargs: Optional[Dict[str, Any]] = {},
152
+ *,
153
+ user_id: Optional[str] = None,
154
+ session_id: Optional[str] = None,
155
+ metadata: Optional[Dict[str, Any]] = None,
156
+ storage: Optional[Storage] = None,
157
+ persist_tool_configs: bool = False
158
+ ):
159
+ """
160
+ Initialize the Tiny Agent.
161
+
162
+ Args:
163
+ model: The model to use with LiteLLM
164
+ api_key: The API key for the model provider
165
+ system_prompt: Custom system prompt for the agent
166
+ logger: Optional logger to use
167
+ session_id: Optional session ID (if provided with storage, will attempt to load existing session)
168
+ metadata: Optional metadata for the session
169
+ storage: Optional storage backend for persistence
170
+ persist_tool_configs: Whether to persist tool configurations
171
+ """
172
+ # Set up logger
173
+ self.logger = logger or logging.getLogger(__name__)
174
+
175
+ # Instead of a single MCPClient, keep multiple:
176
+ self.mcp_clients: List[MCPClient] = []
177
+ # Map from tool_name -> MCPClient instance
178
+ self.tool_to_client: Dict[str, MCPClient] = {}
179
+
180
+ # Simplified hook system - single list of callbacks
181
+ self.callbacks: List[callable] = []
182
+
183
+ # LiteLLM configuration
184
+ self.model = model
185
+ self.api_key = api_key
186
+ self.temperature = temperature
187
+ if model in ["o1", "o1-preview","o3","o4-mini"]:
188
+ self.temperature = 1
189
+ if api_key:
190
+ litellm.api_key = api_key
191
+
192
+ self.model_kwargs = model_kwargs
193
+ self.encoder = tiktoken.get_encoding("o200k_base")
194
+
195
+ # Conversation state
196
+ self.messages = [{
197
+ "role": "system",
198
+ "content": system_prompt or DEFAULT_SYSTEM_PROMPT
199
+ }]
200
+
201
+ # This list now accumulates tools from *all* connected MCP servers:
202
+ self.available_tools: List[Dict[str, Any]] = []
203
+
204
+ # Control flow tools
205
+ self.exit_loop_tools = [
206
+ {
207
+ "type": "function",
208
+ "function": {
209
+ "name": "final_answer",
210
+ "description": "Call this tool when the task given by the user is complete",
211
+ "parameters": {"type": "object", "properties": {"content": {
212
+ "type": "string",
213
+ "description": "Your final answer to the user's problem, user only sees the content of this field. "
214
+ }}}
215
+ ,
216
+ "required": ["content"]
217
+ }
218
+ },
219
+ {
220
+ "type": "function",
221
+ "function": {
222
+ "name": "ask_question",
223
+ "description": "Ask a question to the user to get more info required to solve or clarify their problem.",
224
+ "parameters": {
225
+ "type": "object",
226
+ "properties": {
227
+ "question": {
228
+ "type": "string",
229
+ "description": "The question to ask the user"
230
+ }
231
+ },
232
+ "required": ["question"]
233
+ }
234
+ }
235
+ }
236
+ ]
237
+
238
+ # Add a list to store custom tools (functions and classes)
239
+ self.custom_tools: List[Dict[str, Any]] = []
240
+ self.custom_tool_handlers: Dict[str, Any] = {}
241
+ # 1) User and session management
242
+ self.user_id = user_id or self._generate_session_id()
243
+ self.session_id = session_id or self._generate_session_id()
244
+ # build default metadata
245
+ default_md = {
246
+ "model": model,
247
+ "temperature": temperature,
248
+ **(model_kwargs or {}),
249
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
250
+ }
251
+ self.metadata = metadata or default_md
252
+ self.metadata.setdefault("usage", {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0})
253
+
254
+ # 2) Storage is attached immediately for auto‐saving, but loading is deferred:
255
+ self.storage = storage
256
+ self.persist_tool_configs = persist_tool_configs
257
+ # only a flag — no blocking or hidden runs in __init__
258
+ self._needs_session_load = bool(self.storage and session_id)
259
+
260
+ if self.storage:
261
+ # register auto‐save on llm_end
262
+ self.storage.attach(self)
263
+
264
+ self.logger.debug(f"TinyAgent initialized (session={self.session_id})")
265
+
266
+ # register our usage‐merging hook
267
+ self.add_callback(self._on_llm_end)
268
+
269
+ def _generate_session_id(self) -> str:
270
+ """Produce a unique session identifier."""
271
+ return str(uuid.uuid4())
272
+
273
+ def count_tokens(self, text: str) -> int:
274
+ """Count tokens in a string using tiktoken."""
275
+ if not self.encoder or not text:
276
+ return 0
277
+ try:
278
+ return len(self.encoder.encode(text))
279
+ except Exception as e:
280
+ self.logger.error(f"Error counting tokens: {e}")
281
+ return 0
282
+
283
+ async def save_agent(self) -> None:
284
+ """Persist our full serialized state via the configured Storage."""
285
+ if not self.storage:
286
+ self.logger.warning("No storage configured; skipping save.")
287
+ return
288
+ data = self.to_dict()
289
+ await self.storage.save_session(self.session_id, data, self.user_id)
290
+ self.logger.info(f"Agent state saved for session={self.session_id}")
291
+
292
+ async def _on_llm_end(self, event_name: str, agent: "TinyAgent", **kwargs) -> None:
293
+ """
294
+ Callback hook: after each LLM call, accumulate *all* fields from
295
+ litellm's response.usage into our metadata and persist.
296
+ """
297
+ if event_name != "llm_end":
298
+ return
299
+
300
+ response = kwargs.get("response")
301
+ if response and hasattr(response, "usage") and isinstance(response.usage, dict):
302
+ usage = response.usage
303
+ bucket = self.metadata.setdefault(
304
+ "usage", {}
305
+ )
306
+ # Merge every key from the LLM usage (prompt_tokens, completion_tokens,
307
+ # total_tokens, maybe cost, etc.)
308
+ for field, value in usage.items():
309
+ try:
310
+ # only aggregate numeric fields
311
+ bucket[field] = bucket.get(field, 0) + int(value)
312
+ except (ValueError, TypeError):
313
+ # fallback: overwrite or store as-is
314
+ bucket[field] = value
315
+
316
+ # persist after each LLM call
317
+ await self.save_agent()
318
+
319
+ def to_dict(self) -> Dict[str, Any]:
320
+ """
321
+ Serialize session_id, metadata, and a user‐extensible session_state.
322
+ """
323
+ # start from user's own session_state
324
+ session_data = dict(self.session_state)
325
+ # always include the conversation
326
+ session_data["messages"] = self.messages
327
+
328
+ # optionally include tools
329
+ if self.persist_tool_configs:
330
+ serialized = []
331
+ for cfg in getattr(self, "_tool_configs_for_serialization", []):
332
+ if cfg["type"] == "tiny_agent":
333
+ serialized.append({
334
+ "type": "tiny_agent",
335
+ "state": cfg["state_func"]()
336
+ })
337
+ else:
338
+ serialized.append(cfg)
339
+ session_data["tool_configs"] = serialized
340
+
341
+ return {
342
+ "session_id": self.session_id,
343
+ "metadata": self.metadata,
344
+ "session_state": session_data
345
+ }
346
+
347
+ @classmethod
348
+ def from_dict(
349
+ cls,
350
+ data: Dict[str, Any],
351
+ *,
352
+ logger: Optional[logging.Logger] = None,
353
+ tool_registry: Optional[Dict[str, Any]] = None,
354
+ storage: Optional[Storage] = None
355
+ ) -> "TinyAgent":
356
+ """
357
+ Rehydrate a TinyAgent from JSON state.
358
+ """
359
+ session_id = data["session_id"]
360
+ metadata = data.get("metadata", {})
361
+ state_blob = data.get("session_state", {})
362
+
363
+ # core config
364
+ model = metadata.get("model", "gpt-4.1-mini")
365
+ temperature = metadata.get("temperature", 0.0)
366
+ # everything else except model/temperature/usage → model_kwargs
367
+ model_kwargs = {k:v for k,v in metadata.items() if k not in ("model","temperature","usage")}
368
+
369
+ # instantiate (tools* not yet reconstructed)
370
+ agent = cls(
371
+ model=model,
372
+ api_key=None,
373
+ system_prompt=None,
374
+ temperature=temperature,
375
+ logger=logger,
376
+ model_kwargs=model_kwargs,
377
+ session_id=session_id,
378
+ metadata=metadata,
379
+ storage=storage,
380
+ persist_tool_configs=False # default off
381
+ )
382
+
383
+ # Apply the session data directly instead of loading from storage
384
+ agent._needs_session_load = False
385
+ agent._apply_session_data(data)
386
+
387
+ # rebuild tools if we persisted them
388
+ agent.tool_registry = tool_registry or {}
389
+ for tcfg in state_blob.get("tool_configs", []):
390
+ agent._reconstruct_tool(tcfg)
391
+
392
+ return agent
393
+
394
+ def add_callback(self, callback: callable) -> None:
395
+ """
396
+ Add a callback function to the agent.
397
+
398
+ Args:
399
+ callback: A function that accepts (event_name, agent, **kwargs)
400
+ """
401
+ self.callbacks.append(callback)
402
+
403
+ async def _run_callbacks(self, event_name: str, **kwargs) -> None:
404
+ """
405
+ Run all registered callbacks for an event.
406
+
407
+ Args:
408
+ event_name: The name of the event
409
+ **kwargs: Additional data for the event
410
+ """
411
+ for callback in self.callbacks:
412
+ try:
413
+ self.logger.debug(f"Running callback: {callback}")
414
+ if asyncio.iscoroutinefunction(callback):
415
+ self.logger.debug(f"Callback is a coroutine function")
416
+ await callback(event_name, self, **kwargs)
417
+ else:
418
+ # Check if the callback is a class with an async __call__ method
419
+ if hasattr(callback, '__call__') and asyncio.iscoroutinefunction(callback.__call__):
420
+ self.logger.debug(f"Callback is a class with an async __call__ method")
421
+ await callback(event_name, self, **kwargs)
422
+ else:
423
+ self.logger.debug(f"Callback is a regular function")
424
+ callback(event_name, self, **kwargs)
425
+ except Exception as e:
426
+ self.logger.error(f"Error in callback for {event_name}: {str(e)}")
427
+
428
+ async def connect_to_server(self, command: str, args: List[str],
429
+ include_tools: Optional[List[str]] = None,
430
+ exclude_tools: Optional[List[str]] = None) -> None:
431
+ """
432
+ Connect to an MCP server and fetch available tools.
433
+
434
+ Args:
435
+ command: The command to run the server
436
+ args: List of arguments for the server
437
+ include_tools: Optional list of tool name patterns to include (if provided, only matching tools will be added)
438
+ exclude_tools: Optional list of tool name patterns to exclude (matching tools will be skipped)
439
+ """
440
+ # 1) Create and connect a brand-new client
441
+ client = MCPClient()
442
+
443
+ # Pass our callbacks to the client
444
+ for callback in self.callbacks:
445
+ client.add_callback(callback)
446
+
447
+ await client.connect(command, args)
448
+ self.mcp_clients.append(client)
449
+
450
+ # 2) List tools on *this* server
451
+ resp = await client.session.list_tools()
452
+
453
+ # 3) For each tool, record its schema + map name->client
454
+ added_tools = 0
455
+ for tool in resp.tools:
456
+ # Apply filtering logic
457
+ tool_name = tool.name
458
+
459
+ # Skip if not in include list (when include list is provided)
460
+ if include_tools and not any(pattern in tool_name for pattern in include_tools):
461
+ self.logger.debug(f"Skipping tool {tool_name} - not in include list")
462
+ continue
463
+
464
+ # Skip if in exclude list
465
+ if exclude_tools and any(pattern in tool_name for pattern in exclude_tools):
466
+ self.logger.debug(f"Skipping tool {tool_name} - matched exclude pattern")
467
+ continue
468
+
469
+ fn_meta = {
470
+ "type": "function",
471
+ "function": {
472
+ "name": tool.name,
473
+ "description": tool.description,
474
+ "parameters": tool.inputSchema
475
+ }
476
+ }
477
+ self.available_tools.append(fn_meta)
478
+ self.tool_to_client[tool.name] = client
479
+ added_tools += 1
480
+
481
+ self.logger.info(f"Connected to {command} {args!r}, added {added_tools} tools (filtered from {len(resp.tools)} available)")
482
+ self.logger.debug(f"{command} {args!r} Available tools: {self.available_tools}")
483
+
484
+ def add_tool(self, tool_func_or_class: Any) -> None:
485
+ """
486
+ Add a custom tool (function or class) to the agent.
487
+
488
+ Args:
489
+ tool_func_or_class: A function or class decorated with @tool
490
+ """
491
+ # Check if the tool has the required metadata
492
+ if not hasattr(tool_func_or_class, '_tool_metadata'):
493
+ raise ValueError("Tool must be decorated with @tool decorator")
494
+
495
+ metadata = tool_func_or_class._tool_metadata
496
+
497
+ # Create tool schema
498
+ tool_schema = {
499
+ "type": "function",
500
+ "function": {
501
+ "name": metadata["name"],
502
+ "description": metadata["description"],
503
+ "parameters": metadata["schema"]
504
+ }
505
+ }
506
+
507
+ # Add to available tools
508
+ self.custom_tools.append(tool_schema)
509
+ self.available_tools.append(tool_schema)
510
+
511
+ # Store the handler (function or class)
512
+ self.custom_tool_handlers[metadata["name"]] = tool_func_or_class
513
+
514
+ self.logger.info(f"Added custom tool: {metadata['name']}")
515
+
516
+ def add_tools(self, tools: List[Any]) -> None:
517
+ """
518
+ Add multiple custom tools to the agent.
519
+
520
+ Args:
521
+ tools: List of functions or classes decorated with @tool
522
+ """
523
+ for tool_func_or_class in tools:
524
+ self.add_tool(tool_func_or_class)
525
+
526
+ async def _execute_custom_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> str:
527
+ """
528
+ Execute a custom tool and return its result.
529
+
530
+ Args:
531
+ tool_name: Name of the tool to execute
532
+ tool_args: Arguments for the tool
533
+
534
+ Returns:
535
+ String result from the tool
536
+ """
537
+ handler = self.custom_tool_handlers.get(tool_name)
538
+ if not handler:
539
+ return f"Error: Tool '{tool_name}' not found"
540
+
541
+ try:
542
+ # Check if it's a class or function
543
+ metadata = handler._tool_metadata
544
+
545
+ if metadata["is_class"]:
546
+ # Instantiate the class and call it
547
+ instance = handler(**tool_args)
548
+ if hasattr(instance, "__call__"):
549
+ result = instance()
550
+ else:
551
+ result = instance
552
+ else:
553
+ # Call the function directly
554
+ result = handler(**tool_args)
555
+
556
+ # Handle async functions
557
+ if asyncio.iscoroutine(result):
558
+ result = await result
559
+
560
+ return str(result)
561
+ except Exception as e:
562
+ self.logger.error(f"Error executing custom tool {tool_name}: {str(e)}")
563
+ return f"Error executing tool {tool_name}: {str(e)}"
564
+
565
+ async def run(self, user_input: str, max_turns: int = 10) -> str:
566
+ # ----------------------------------------------------------------
567
+ # Ensure any deferred session‐load happens exactly once
568
+ # ----------------------------------------------------------------
569
+ if self._needs_session_load:
570
+ self.logger.debug(f"Deferred session load detected for {self.session_id}; loading now")
571
+ await self.init_async()
572
+
573
+ # ----------------------------------------------------------------
574
+ # Now proceed with the normal agent loop
575
+ # ----------------------------------------------------------------
576
+
577
+ # Notify start
578
+ await self._run_callbacks("agent_start", user_input=user_input)
579
+
580
+ # Add user message to conversation with timestamp
581
+ user_message = {
582
+ "role": "user",
583
+ "content": user_input,
584
+ "created_at": int(time.time())
585
+ }
586
+ self.messages.append(user_message)
587
+ await self._run_callbacks("message_add", message=self.messages[-1])
588
+
589
+ # Initialize loop control variables
590
+ num_turns = 0
591
+ next_turn_should_call_tools = True
592
+
593
+ # The main agent loop
594
+ while True:
595
+ # Get all available tools including exit loop tools
596
+ all_tools = self.available_tools + self.exit_loop_tools
597
+
598
+ # Call LLM with messages and tools
599
+ try:
600
+ self.logger.info(f"Calling LLM with {len(self.messages)} messages and {len(all_tools)} tools")
601
+
602
+ # Notify LLM start
603
+ await self._run_callbacks("llm_start", messages=self.messages, tools=all_tools)
604
+
605
+ response = await litellm.acompletion(
606
+ model=self.model,
607
+ messages=self.messages,
608
+ tools=all_tools,
609
+ tool_choice="auto",
610
+ temperature=self.temperature,
611
+ **self.model_kwargs
612
+ )
613
+
614
+ # Notify LLM end
615
+ await self._run_callbacks("llm_end", response=response)
616
+
617
+ # Process the response - properly handle the object
618
+ response_message = response.choices[0].message
619
+ self.logger.debug(f"🔥🔥🔥🔥🔥🔥 Response : {response_message}")
620
+
621
+ # Extract both content and any tool_calls
622
+ content = getattr(response_message, "content", "") or ""
623
+ tool_calls = getattr(response_message, "tool_calls", []) or []
624
+ has_tool_calls = bool(tool_calls)
625
+
626
+ # Now emit the "assistant" message that carries the function call (or, if no calls, the content)
627
+ if has_tool_calls:
628
+ assistant_msg = {
629
+ "role": "assistant",
630
+ "content": content, # split off above
631
+ "tool_calls": tool_calls,
632
+ "created_at": int(time.time())
633
+ }
634
+ else:
635
+ assistant_msg = {
636
+ "role": "assistant",
637
+ "content": content,
638
+ "created_at": int(time.time())
639
+ }
640
+ self.messages.append(assistant_msg)
641
+ await self._run_callbacks("message_add", message=assistant_msg)
642
+
643
+ # Process tool calls if they exist
644
+ if has_tool_calls:
645
+ self.logger.info(f"Tool calls detected: {len(tool_calls)}")
646
+
647
+ # Process each tool call one by one
648
+ for tool_call in tool_calls:
649
+ tool_call_id = tool_call.id
650
+ function_info = tool_call.function
651
+ tool_name = function_info.name
652
+
653
+ # Create a tool message
654
+ tool_message = {
655
+ "role": "tool",
656
+ "tool_call_id": tool_call_id,
657
+ "name": tool_name,
658
+ "content": "", # Default empty content
659
+ "created_at": int(time.time())
660
+ }
661
+
662
+ try:
663
+ # Parse tool arguments
664
+ try:
665
+ tool_args = json.loads(function_info.arguments)
666
+ except json.JSONDecodeError:
667
+ self.logger.error(f"Could not parse tool arguments: {function_info.arguments}")
668
+ tool_args = {}
669
+
670
+ # Handle control flow tools
671
+ if tool_name == "final_answer":
672
+ # Add a response for this tool call before returning
673
+ tool_message["content"] = tool_args.get("content", "Task completed without final answer.!!!")
674
+ self.messages.append(tool_message)
675
+ await self._run_callbacks("message_add", message=tool_message)
676
+ await self._run_callbacks("agent_end", result="Task completed.")
677
+ return tool_message["content"]
678
+ elif tool_name == "ask_question":
679
+ question = tool_args.get("question", "Could you provide more details?")
680
+ # Add a response for this tool call before returning
681
+ tool_message["content"] = f"Question asked: {question}"
682
+ self.messages.append(tool_message)
683
+ await self._run_callbacks("message_add", message=tool_message)
684
+ await self._run_callbacks("agent_end", result=f"I need more information: {question}")
685
+ return f"I need more information: {question}"
686
+ else:
687
+ # Check if it's a custom tool first
688
+ if tool_name in self.custom_tool_handlers:
689
+ tool_message["content"] = await self._execute_custom_tool(tool_name, tool_args)
690
+ else:
691
+ # Dispatch to the proper MCPClient
692
+ client = self.tool_to_client.get(tool_name)
693
+ if not client:
694
+ tool_message["content"] = f"No MCP server registered for tool '{tool_name}'"
695
+ else:
696
+ try:
697
+ self.logger.debug(f"Calling tool {tool_name} with args: {tool_args}")
698
+ self.logger.debug(f"Client: {client}")
699
+ content_list = await client.call_tool(tool_name, tool_args)
700
+ self.logger.debug(f"Tool {tool_name} returned: {content_list}")
701
+ # Safely extract text from the content
702
+ if content_list:
703
+ # Try different ways to extract the content
704
+ if hasattr(content_list[0], 'text'):
705
+ tool_message["content"] = content_list[0].text
706
+ elif isinstance(content_list[0], dict) and 'text' in content_list[0]:
707
+ tool_message["content"] = content_list[0]['text']
708
+ else:
709
+ tool_message["content"] = str(content_list)
710
+ else:
711
+ tool_message["content"] = "Tool returned no content"
712
+ except Exception as e:
713
+ self.logger.error(f"Error calling tool {tool_name}: {str(e)}")
714
+ tool_message["content"] = f"Error executing tool {tool_name}: {str(e)}"
715
+ except Exception as e:
716
+ # If any error occurs during tool call processing, make sure we still have a tool response
717
+ self.logger.error(f"Unexpected error processing tool call {tool_call_id}: {str(e)}")
718
+ tool_message["content"] = f"Error processing tool call: {str(e)}"
719
+
720
+ # Always add the tool message to ensure each tool call has a response
721
+ self.messages.append(tool_message)
722
+ await self._run_callbacks("message_add", message=tool_message)
723
+
724
+ next_turn_should_call_tools = False
725
+ else:
726
+ # No tool calls in this message
727
+ if num_turns > 0:
728
+ #if next_turn_should_call_tools and num_turns > 0:
729
+ # If we expected tool calls but didn't get any, we're done
730
+ await self._run_callbacks("agent_end", result=assistant_msg["content"] or "")
731
+ return assistant_msg["content"] or ""
732
+
733
+ next_turn_should_call_tools = True
734
+
735
+ num_turns += 1
736
+ if num_turns >= max_turns:
737
+ result = "Max turns reached. Task incomplete."
738
+ await self._run_callbacks("agent_end", result=result)
739
+ return result
740
+
741
+ except Exception as e:
742
+ self.logger.error(f"Error in agent loop: {str(e)}")
743
+ result = f"Error: {str(e)}"
744
+ await self._run_callbacks("agent_end", result=result, error=str(e))
745
+ return result
746
+
747
+
748
+ async def close(self):
749
+ """Clean up *all* MCP clients."""
750
+ for client in self.mcp_clients:
751
+ try:
752
+ await client.close()
753
+ except RuntimeError as e:
754
+ self.logger.error(f"Error closing MCP client: {str(e)}")
755
+ # Continue closing other clients even if one fails
756
+ def clear_conversation(self):
757
+ """
758
+ Clear the conversation history, preserving only the initial system prompt.
759
+ """
760
+ if self.messages:
761
+ system_msg = self.messages[0]
762
+ else:
763
+ # Rebuild a default system prompt if somehow missing
764
+ default_sys = {
765
+ "role": "system",
766
+ "content": (
767
+ "You are a helpful AI assistant with access to a variety of tools. "
768
+ "Use the tools when appropriate to accomplish tasks. "
769
+ "If a tool you need isn't available, just say so."
770
+ )
771
+ }
772
+ system_msg = default_sys
773
+ self.messages = [system_msg]
774
+ self.logger.info("TinyAgent conversation history cleared.")
775
+
776
+ def as_tool(self, name: Optional[str] = None, description: Optional[str] = None) -> Dict[str, Any]:
777
+ """
778
+ Convert this TinyAgent instance into a tool that can be used by another TinyAgent.
779
+
780
+ Args:
781
+ name: Optional custom name for the tool (defaults to "TinyAgentTool")
782
+ description: Optional description (defaults to a generic description)
783
+
784
+ Returns:
785
+ A tool function that can be added to another TinyAgent
786
+ """
787
+ tool_name = name or f"TinyAgentTool_{id(self)}"
788
+ tool_description = description or f"A tool that uses a TinyAgent with model {self.model} to solve tasks"
789
+
790
+ @tool(name=tool_name, description=tool_description)
791
+ async def agent_tool(query: str, max_turns: int = 5) -> str:
792
+ """
793
+ Run this TinyAgent with the given query.
794
+
795
+ Args:
796
+ query: The task or question to process
797
+ max_turns: Maximum number of turns (default: 5)
798
+
799
+ Returns:
800
+ The agent's response
801
+ """
802
+ return await self.run(query, max_turns=max_turns)
803
+
804
+ return agent_tool
805
+
806
+ async def init_async(self) -> "TinyAgent":
807
+ """
808
+ Load session data from storage if flagged. Safe to call only once.
809
+ """
810
+ if not self._needs_session_load:
811
+ return self
812
+
813
+ try:
814
+ data = await self.storage.load_session(self.session_id, self.user_id)
815
+ if data:
816
+ self.logger.info(f"Resuming session {self.session_id}")
817
+ self._apply_session_data(data)
818
+ else:
819
+ self.logger.info(f"No existing session for {self.session_id}")
820
+ except Exception as e:
821
+ self.logger.error(f"Failed to load session {self.session_id}: {traceback.format_exc()}")
822
+ finally:
823
+ self._needs_session_load = False
824
+
825
+ return self
826
+
827
+ @classmethod
828
+ async def create(
829
+ cls,
830
+ model: str = "gpt-4.1-mini",
831
+ api_key: Optional[str] = None,
832
+ system_prompt: Optional[str] = None,
833
+ temperature: float = 0.0,
834
+ logger: Optional[logging.Logger] = None,
835
+ model_kwargs: Optional[Dict[str, Any]] = {},
836
+ *,
837
+ user_id: Optional[str] = None,
838
+ session_id: Optional[str] = None,
839
+ metadata: Optional[Dict[str, Any]] = None,
840
+ storage: Optional[Storage] = None,
841
+ persist_tool_configs: bool = False
842
+ ) -> "TinyAgent":
843
+ """
844
+ Async factory: constructs the agent, then loads an existing session
845
+ if (storage and session_id) were provided.
846
+ """
847
+ agent = cls(
848
+ model=model,
849
+ api_key=api_key,
850
+ system_prompt=system_prompt,
851
+ temperature=temperature,
852
+ logger=logger,
853
+ model_kwargs=model_kwargs,
854
+ user_id=user_id,
855
+ session_id=session_id,
856
+ metadata=metadata,
857
+ storage=storage,
858
+ persist_tool_configs=persist_tool_configs
859
+ )
860
+ if agent._needs_session_load:
861
+ await agent.init_async()
862
+ return agent
863
+
864
+ def _apply_session_data(self, data: Dict[str, Any]) -> None:
865
+ """
866
+ Apply loaded session data to this agent instance.
867
+
868
+ Args:
869
+ data: Session data dictionary from storage
870
+ """
871
+ # Update metadata (preserving model and temperature from constructor)
872
+ if "metadata" in data:
873
+ # Keep original model/temperature/api_key but merge everything else
874
+ stored_metadata = data["metadata"]
875
+ for key, value in stored_metadata.items():
876
+ if key not in ("model", "temperature"): # Don't override these
877
+ self.metadata[key] = value
878
+
879
+ # Load session state
880
+ if "session_state" in data:
881
+ state_blob = data["session_state"]
882
+
883
+ # Restore conversation history
884
+ if "messages" in state_blob:
885
+ self.messages = state_blob["messages"]
886
+
887
+ # Restore other session state
888
+ for key, value in state_blob.items():
889
+ if key != "messages" and key != "tool_configs":
890
+ self.session_state[key] = value
891
+
892
+ # Tool configs would be handled separately if needed
893
+
894
+ async def run_example():
895
+ """Example usage of TinyAgent with proper logging."""
896
+ import os
897
+ import sys
898
+ from tinyagent.hooks.logging_manager import LoggingManager
899
+ from tinyagent.hooks.rich_ui_callback import RichUICallback
900
+
901
+ # Create and configure logging manager
902
+ log_manager = LoggingManager(default_level=logging.INFO)
903
+ log_manager.set_levels({
904
+ 'tinyagent.tiny_agent': logging.DEBUG, # Debug for this module
905
+ 'tinyagent.mcp_client': logging.INFO,
906
+ 'tinyagent.hooks.rich_ui_callback': logging.INFO,
907
+ })
908
+
909
+ # Configure a console handler
910
+ console_handler = logging.StreamHandler(sys.stdout)
911
+ log_manager.configure_handler(
912
+ console_handler,
913
+ format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
914
+ level=logging.DEBUG
915
+ )
916
+
917
+ # Get module-specific loggers
918
+ agent_logger = log_manager.get_logger('tinyagent.tiny_agent')
919
+ ui_logger = log_manager.get_logger('tinyagent.hooks.rich_ui_callback')
920
+ mcp_logger = log_manager.get_logger('tinyagent.mcp_client')
921
+
922
+ agent_logger.debug("Starting TinyAgent example")
923
+
924
+ # Get API key from environment
925
+ api_key = os.environ.get("OPENAI_API_KEY")
926
+ if not api_key:
927
+ agent_logger.error("Please set the OPENAI_API_KEY environment variable")
928
+ return
929
+
930
+ # Initialize the agent with our logger
931
+ agent = await TinyAgent.create(
932
+ model="gpt-4.1-mini",
933
+ api_key=api_key,
934
+ logger=agent_logger,
935
+ session_id="my-session-123",
936
+ storage=None
937
+ )
938
+
939
+ # Add the Rich UI callback with our logger
940
+ rich_ui = RichUICallback(
941
+ markdown=True,
942
+ show_message=True,
943
+ show_thinking=True,
944
+ show_tool_calls=True,
945
+ logger=ui_logger
946
+ )
947
+ agent.add_callback(rich_ui)
948
+
949
+ # Run the agent with a user query
950
+ user_input = "What is the capital of France?"
951
+ agent_logger.info(f"Running agent with input: {user_input}")
952
+ result = await agent.run(user_input)
953
+
954
+ agent_logger.info(f"Final result: {result}")
955
+
956
+ # Clean up
957
+ await agent.close()
958
+ agent_logger.debug("Example completed")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyagent-py
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Tiny Agent with MCP Client
5
5
  Author-email: Mahdi Golchin <golchin@askdev.ai>
6
6
  Project-URL: Homepage, https://github.com/askbudi/tinyagent
@@ -31,11 +31,17 @@ Requires-Dist: aiosqlite>=0.18.0; extra == "all"
31
31
  Requires-Dist: gradio>=3.50.0; extra == "all"
32
32
  Dynamic: license-file
33
33
 
34
- # tinyagent
35
- Tiny Agent: 100 lines Agent with MCP
34
+ # TinyAgent
35
+ Tiny Agent: 100 lines Agent with MCP and extendable hook system
36
+
37
+ [![AskDev.AI | Chat with TinyAgent](https://img.shields.io/badge/AskDev.AI-Chat_with_TinyAgent-blue?style=flat-square)](https://askdev.ai/github/askbudi/tinyagent)
38
+
39
+
36
40
  ![TinyAgent Logo](https://raw.githubusercontent.com/askbudi/tinyagent/main/public/logo.png)
37
41
 
38
42
 
43
+ [![AskDev.AI | Chat with TinyAgent](https://img.shields.io/badge/AskDev.AI-Chat_with_TinyAgent-blue?style=flat-square)](https://askdev.ai/github/askbudi/tinyagent)
44
+
39
45
 
40
46
  Inspired by:
41
47
  - [Tiny Agents blog post](https://huggingface.co/blog/tiny-agents)
@@ -91,6 +97,8 @@ uv pip install tinyagent-py[dev]
91
97
  ```
92
98
 
93
99
  ## Usage
100
+ [![AskDev.AI | Chat with TinyAgent](https://img.shields.io/badge/AskDev.AI-Chat_with_TinyAgent-blue?style=flat-square)](https://askdev.ai/github/askbudi/tinyagent)
101
+
94
102
 
95
103
  ```python
96
104
  from tinyagent import TinyAgent
@@ -239,6 +247,14 @@ if __name__ == "__main__":
239
247
  ```
240
248
  ---
241
249
 
250
+ ## Build your own TinyAgent
251
+
252
+ You can chat with TinyAgent and build your own TinyAgent for your use case.
253
+
254
+ [![AskDev.AI | Chat with TinyAgent](https://img.shields.io/badge/AskDev.AI-Chat_with_TinyAgent-blue?style=flat-square)](https://askdev.ai/github/askbudi/tinyagent)
255
+
256
+ ---
257
+
242
258
  ## Contributing Hooks
243
259
 
244
260
  - Place new hooks in the `tinyagent/hooks/` directory.
@@ -0,0 +1,20 @@
1
+ tinyagent/__init__.py,sha256=GrD21npMQGzl9ZYKYTP8VxHLzCfJCvA0oTKQZTkmnCw,117
2
+ tinyagent/mcp_client.py,sha256=ZyhIWFQvOveNVhMKkMkUMYdbqRSuqjK_m-ApP2llFVE,6215
3
+ tinyagent/tiny_agent.py,sha256=89OLYj95VUW7uoHpkWXZjmxsQISUMjHffUmDT-3-Nd4,39250
4
+ tinyagent/hooks/__init__.py,sha256=UztCHjoqF5JyDolbWwkBsBZkWguDQg23l2GD_zMHt-s,178
5
+ tinyagent/hooks/agno_storage_hook.py,sha256=5qvvjmtraanPa-A46Zstrqq3s1e-sC7Ly0o3zifuw_4,5003
6
+ tinyagent/hooks/gradio_callback.py,sha256=jGsZlObAd6I5lN9cE53dDL_LfiB8I0tBsicuHwwmL-M,44833
7
+ tinyagent/hooks/logging_manager.py,sha256=UpdmpQ7HRPyer-jrmQSXcBwi409tV9LnGvXSHjTcYTI,7935
8
+ tinyagent/hooks/rich_ui_callback.py,sha256=5iCNOiJmhc1lOL7ZjaOt5Sk3rompko4zu_pAxfTVgJQ,22897
9
+ tinyagent/storage/__init__.py,sha256=NebvYxwEGJtvPnRO9dGa-bgOwA7cPkLjFHnMWDxMg5I,261
10
+ tinyagent/storage/agno_storage.py,sha256=ol4qwdH-9jYjBjDvsYkHh7I-vu8uHArPtQylUpoEaCc,4322
11
+ tinyagent/storage/base.py,sha256=GGAMvOoslmm1INLFG_jtwOkRk2Qg39QXx-1LnN7fxDI,1474
12
+ tinyagent/storage/json_file_storage.py,sha256=SYD8lvTHu2-FEHm1tZmsrcgEOirBrlUsUM186X-UPgI,1114
13
+ tinyagent/storage/postgres_storage.py,sha256=IGwan8UXHNnTZFK1F8x4kvMDex3GAAGWUg9ePx_5IF4,9018
14
+ tinyagent/storage/redis_storage.py,sha256=hu3y7wHi49HkpiR-AW7cWVQuTVOUk1WaB8TEPGUKVJ8,1742
15
+ tinyagent/storage/sqlite_storage.py,sha256=7lk1XZpr2t4s2bjVr9-AqrI74w4hwkuK3taWtyJZhBc,5769
16
+ tinyagent_py-0.0.5.dist-info/licenses/LICENSE,sha256=YIogcVQnknaaE4K-oaQylFWo8JGRBWnwmGb3fWB_Pww,1064
17
+ tinyagent_py-0.0.5.dist-info/METADATA,sha256=EW9xVFf0vYbe1DQYxQAlNgOXUzuP9g2pqc__yx4V8NQ,9054
18
+ tinyagent_py-0.0.5.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
19
+ tinyagent_py-0.0.5.dist-info/top_level.txt,sha256=Ny8aJNchZpc2Vvhp3306L5vjceJakvFxBk-UjjVeA_I,10
20
+ tinyagent_py-0.0.5.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ tinyagent
@@ -1,17 +0,0 @@
1
- hooks/__init__.py,sha256=UztCHjoqF5JyDolbWwkBsBZkWguDQg23l2GD_zMHt-s,178
2
- hooks/agno_storage_hook.py,sha256=5qvvjmtraanPa-A46Zstrqq3s1e-sC7Ly0o3zifuw_4,5003
3
- hooks/gradio_callback.py,sha256=jGsZlObAd6I5lN9cE53dDL_LfiB8I0tBsicuHwwmL-M,44833
4
- hooks/logging_manager.py,sha256=UpdmpQ7HRPyer-jrmQSXcBwi409tV9LnGvXSHjTcYTI,7935
5
- hooks/rich_ui_callback.py,sha256=5iCNOiJmhc1lOL7ZjaOt5Sk3rompko4zu_pAxfTVgJQ,22897
6
- storage/__init__.py,sha256=NebvYxwEGJtvPnRO9dGa-bgOwA7cPkLjFHnMWDxMg5I,261
7
- storage/agno_storage.py,sha256=ol4qwdH-9jYjBjDvsYkHh7I-vu8uHArPtQylUpoEaCc,4322
8
- storage/base.py,sha256=GGAMvOoslmm1INLFG_jtwOkRk2Qg39QXx-1LnN7fxDI,1474
9
- storage/json_file_storage.py,sha256=SYD8lvTHu2-FEHm1tZmsrcgEOirBrlUsUM186X-UPgI,1114
10
- storage/postgres_storage.py,sha256=IGwan8UXHNnTZFK1F8x4kvMDex3GAAGWUg9ePx_5IF4,9018
11
- storage/redis_storage.py,sha256=hu3y7wHi49HkpiR-AW7cWVQuTVOUk1WaB8TEPGUKVJ8,1742
12
- storage/sqlite_storage.py,sha256=7lk1XZpr2t4s2bjVr9-AqrI74w4hwkuK3taWtyJZhBc,5769
13
- tinyagent_py-0.0.4.dist-info/licenses/LICENSE,sha256=YIogcVQnknaaE4K-oaQylFWo8JGRBWnwmGb3fWB_Pww,1064
14
- tinyagent_py-0.0.4.dist-info/METADATA,sha256=MDvRoleb36ya8z44BxQvtgSFJ_-WfH5kv7eSWeaMdJQ,8254
15
- tinyagent_py-0.0.4.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
16
- tinyagent_py-0.0.4.dist-info/top_level.txt,sha256=PfpFqZliMhzue7YU7RrBiZGoAqVBPr9sRc310dWabug,14
17
- tinyagent_py-0.0.4.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- hooks
2
- storage
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes