kader 0.1.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.
kader/agent/logger.py ADDED
@@ -0,0 +1,170 @@
1
+ """
2
+ Logger module for Kader agents.
3
+
4
+ This module provides logging functionality for agents with memory sessions.
5
+ Logs are written to files in ~/.kader/logs without affecting agent performance.
6
+ Only agents with memory sessions will generate logs.
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from threading import Lock
12
+ from typing import Any, Dict, Optional
13
+
14
+ from loguru import logger
15
+
16
+
17
+ class AgentLogger:
18
+ """
19
+ Logger class for Kader agents that logs to files with thread-safe operations.
20
+ Only agents with memory sessions will be logged.
21
+ """
22
+
23
+ def __init__(self):
24
+ self._loggers = {}
25
+ self._lock = Lock()
26
+
27
+ def setup_logger(
28
+ self, agent_name: str, session_id: Optional[str] = None
29
+ ) -> Optional[str]:
30
+ """
31
+ Set up logger for an agent with memory session.
32
+
33
+ Args:
34
+ agent_name: Name of the agent
35
+ session_id: Session ID for the agent
36
+
37
+ Returns:
38
+ Logger ID if successful, None otherwise
39
+ """
40
+ if not session_id:
41
+ # Only log agents with memory sessions
42
+ return None
43
+
44
+ # Create log file path
45
+ logs_dir = Path.home() / ".kader" / "logs"
46
+ logs_dir.mkdir(parents=True, exist_ok=True)
47
+
48
+ log_filename = f"{agent_name}_{session_id}.log"
49
+ log_file_path = logs_dir / log_filename
50
+
51
+ logger_id = f"{agent_name}_{session_id}"
52
+
53
+ # Add file sink with thread-safe configuration
54
+ with self._lock:
55
+ if logger_id not in self._loggers:
56
+ # Remove default handler to avoid console output
57
+ # We don't remove default handlers globally as other parts of the app might use them
58
+ # Instead, create a new logger instance for our file logging
59
+ new_logger = logger.bind(name=logger_id)
60
+
61
+ # Remove all existing handlers from this logger instance
62
+ new_logger.remove()
63
+
64
+ # Add file sink with rotation and compression - NO CONSOLE OUTPUT
65
+ handler_id = new_logger.add(
66
+ log_file_path,
67
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
68
+ level="INFO",
69
+ rotation="10 MB",
70
+ retention="7 days",
71
+ compression="zip",
72
+ enqueue=True, # Enables thread-safe logging in a separate thread
73
+ serialize=False,
74
+ backtrace=True,
75
+ diagnose=True,
76
+ )
77
+
78
+ self._loggers[logger_id] = (new_logger, handler_id)
79
+ return logger_id
80
+
81
+ return logger_id
82
+
83
+ def log_token_usage(
84
+ self,
85
+ logger_id: str,
86
+ prompt_tokens: int,
87
+ completion_tokens: int,
88
+ total_tokens: int,
89
+ ):
90
+ """Log token usage information."""
91
+ if logger_id in self._loggers:
92
+ logger_instance, _ = self._loggers[logger_id]
93
+ logger_instance.info(
94
+ f"TOKEN_USAGE | Prompt tokens: {prompt_tokens}, "
95
+ f"Completion tokens: {completion_tokens}, "
96
+ f"Total tokens: {total_tokens}"
97
+ )
98
+
99
+ def calculate_cost(
100
+ self,
101
+ logger_id: str,
102
+ total_cost: float,
103
+ ):
104
+ """Calculate and log cost based on token usage."""
105
+
106
+ self.log_cost(logger_id, total_cost)
107
+ return total_cost
108
+
109
+ def log_llm_response(self, logger_id: str, response: Any):
110
+ """Log LLM response."""
111
+ if logger_id in self._loggers:
112
+ logger_instance, _ = self._loggers[logger_id]
113
+ logger_instance.info(f"LLM_RESPONSE | Response: {response}")
114
+
115
+ def log_tool_usage(self, logger_id: str, tool_name: str, arguments: Dict[str, Any]):
116
+ """Log tool usage with arguments."""
117
+ if logger_id in self._loggers:
118
+ logger_instance, _ = self._loggers[logger_id]
119
+ logger_instance.info(
120
+ f"TOOL_USAGE | Tool: {tool_name}, Arguments: {json.dumps(arguments)}"
121
+ )
122
+
123
+ def log_cost(self, logger_id: str, cost: float):
124
+ """Log cost information."""
125
+ if logger_id in self._loggers:
126
+ logger_instance, _ = self._loggers[logger_id]
127
+ logger_instance.info(f"COST | Cost: ${cost:.6f}")
128
+
129
+ def log_interaction(
130
+ self,
131
+ logger_id: str,
132
+ input_msg: str,
133
+ output_msg: str,
134
+ token_usage: Optional[Dict[str, int]] = None,
135
+ cost: Optional[float] = None,
136
+ tools_used: Optional[Dict[str, Any]] = None,
137
+ ):
138
+ """Log a complete agent interaction with all relevant information."""
139
+ if logger_id in self._loggers:
140
+ logger_instance, _ = self._loggers[logger_id]
141
+
142
+ log_parts = [f"INTERACTION | Input: {input_msg}"]
143
+
144
+ if output_msg:
145
+ log_parts.append(f"Output: {output_msg}")
146
+
147
+ if token_usage:
148
+ log_parts.append(
149
+ f"Tokens - Prompt: {token_usage.get('prompt_tokens', 0)}, "
150
+ f"Completion: {token_usage.get('completion_tokens', 0)}, "
151
+ f"Total: {token_usage.get('total_tokens', 0)}"
152
+ )
153
+
154
+ if cost is not None:
155
+ log_parts.append(f"Cost: ${cost:.6f}")
156
+
157
+ if tools_used:
158
+ log_parts.append(f"Tools: {json.dumps(tools_used)}")
159
+
160
+ logger_instance.info(" | ".join(log_parts))
161
+
162
+ def log_event(self, logger_id: str, event_type: str, data: Dict[str, Any]):
163
+ """Log a general event with custom data."""
164
+ if logger_id in self._loggers:
165
+ logger_instance, _ = self._loggers[logger_id]
166
+ logger_instance.info(f"{event_type.upper()} | {json.dumps(data)}")
167
+
168
+
169
+ # Global logger instance
170
+ agent_logger = AgentLogger()
kader/config.py ADDED
@@ -0,0 +1,139 @@
1
+ """
2
+ Kader configuration management module.
3
+
4
+ This module handles the creation and management of the .kader directory
5
+ in the user's home directory, including creating the required .env file
6
+ with OLLAMA_API_KEY and loading environment variables.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ from pathlib import Path
12
+
13
+
14
+ def load_env_file(env_file_path):
15
+ """
16
+ Load environment variables from a .env file.
17
+ This function reads the .env file and sets the variables in os.environ.
18
+ """
19
+ if not env_file_path.exists():
20
+ return False
21
+
22
+ try:
23
+ with open(env_file_path, "r", encoding="utf-8") as file:
24
+ for line in file:
25
+ line = line.strip()
26
+ # Skip comments and empty lines
27
+ if line and not line.startswith("#") and "=" in line:
28
+ key, value = line.split("=", 1)
29
+ key = key.strip()
30
+ value = value.strip()
31
+
32
+ # Remove surrounding quotes if present
33
+ if value.startswith('"') and value.endswith('"'):
34
+ value = value[1:-1]
35
+ elif value.startswith("'") and value.endswith("'"):
36
+ value = value[1:-1]
37
+
38
+ # Set the environment variable if it's not already set
39
+ if key not in os.environ:
40
+ os.environ[key] = value
41
+ return True
42
+ except Exception as e:
43
+ print(f"Error loading .env file: {str(e)}")
44
+ return False
45
+
46
+
47
+ def get_kader_directory():
48
+ """Get the path to the .kader directory in the user's home directory."""
49
+ return Path.home() / ".kader"
50
+
51
+
52
+ def ensure_kader_directory():
53
+ """
54
+ Ensure that the .kader directory exists in the user's home directory.
55
+ Creates it if it doesn't exist.
56
+ """
57
+ kader_dir = get_kader_directory()
58
+
59
+ # Create the directory if it doesn't exist
60
+ kader_dir.mkdir(exist_ok=True)
61
+
62
+ # Ensure the directory has appropriate permissions on Unix-like systems
63
+ if not sys.platform.startswith("win"):
64
+ kader_dir.chmod(0o755)
65
+
66
+ return kader_dir
67
+
68
+
69
+ def ensure_env_file(kader_dir):
70
+ """
71
+ Ensure that the .env file exists in the .kader directory with the
72
+ required OLLAMA_API_KEY configuration.
73
+ """
74
+ env_file = kader_dir / ".env"
75
+
76
+ # Create the .env file if it doesn't exist
77
+ if not env_file.exists():
78
+ env_file.write_text("OLLAMA_API_KEY=''\n", encoding="utf-8")
79
+
80
+ # Set appropriate permissions for the .env file on Unix-like systems
81
+ if not sys.platform.startswith("win"):
82
+ env_file.chmod(0o644)
83
+
84
+ return env_file
85
+
86
+
87
+ def initialize_kader_config():
88
+ """
89
+ Initialize the .kader directory in the user's home directory with required configuration files.
90
+ This function creates the directory, sets up the .env file with OLLAMA_API_KEY,
91
+ and loads all environment variables from the .env file.
92
+ """
93
+ try:
94
+ # Ensure the .kader directory exists
95
+ kader_dir = ensure_kader_directory()
96
+
97
+ # Ensure the .env file exists with the required configuration
98
+ ensure_env_file(kader_dir)
99
+
100
+ # Load environment variables from the .env file
101
+ env_file_path = kader_dir / ".env"
102
+ load_env_file(env_file_path)
103
+
104
+ # Optionally add the .kader directory to the Python path so it can be accessed
105
+ kader_dir_str = str(kader_dir)
106
+ if kader_dir_str not in sys.path:
107
+ sys.path.insert(0, kader_dir_str)
108
+
109
+ return kader_dir, True
110
+
111
+ except PermissionError as e:
112
+ print(
113
+ f"Permission denied: Unable to create .kader directory in {Path.home()}. {str(e)}"
114
+ )
115
+ return None, False
116
+ except Exception as e:
117
+ print(f"Error initializing Kader config: {str(e)}")
118
+ return None, False
119
+
120
+
121
+ # Initialize the configuration when the module is imported
122
+ kader_dir, success = initialize_kader_config()
123
+
124
+ if success:
125
+ # Define constants for other modules to use
126
+ KADER_DIR = kader_dir
127
+ ENV_FILE_PATH = kader_dir / ".env"
128
+
129
+ __version__ = "0.1.0"
130
+ __author__ = "Kader Project"
131
+ __all__ = [
132
+ "KADER_DIR",
133
+ "ENV_FILE_PATH",
134
+ "initialize_kader_config",
135
+ "get_kader_directory",
136
+ "ensure_kader_directory",
137
+ "ensure_env_file",
138
+ "load_env_file",
139
+ ]
@@ -0,0 +1,66 @@
1
+ """
2
+ Kader Memory Module
3
+
4
+ Provides memory management for agents following the AWS Strands agents SDK hierarchy:
5
+ - State Management: AgentState for persistent state, RequestState for request-scoped context
6
+ - Session Management: FileSessionManager for filesystem-based persistence
7
+ - Conversation Management: SlidingWindowConversationManager for context windowing
8
+
9
+ Memory is stored locally in $HOME/.kader/memory as directories and JSON files.
10
+ """
11
+
12
+ # Core types
13
+ # Conversation management
14
+ from .conversation import (
15
+ ConversationManager,
16
+ ConversationMessage,
17
+ NullConversationManager,
18
+ SlidingWindowConversationManager,
19
+ )
20
+
21
+ # Session management
22
+ from .session import (
23
+ FileSessionManager,
24
+ Session,
25
+ SessionManager,
26
+ )
27
+
28
+ # State management
29
+ from .state import (
30
+ AgentState,
31
+ RequestState,
32
+ )
33
+ from .types import (
34
+ MemoryConfig,
35
+ SessionType,
36
+ decode_bytes_values,
37
+ encode_bytes_values,
38
+ get_default_memory_dir,
39
+ get_timestamp,
40
+ load_json,
41
+ save_json,
42
+ )
43
+
44
+ __all__ = [
45
+ # Types
46
+ "SessionType",
47
+ "MemoryConfig",
48
+ "get_timestamp",
49
+ "get_default_memory_dir",
50
+ "save_json",
51
+ "load_json",
52
+ "encode_bytes_values",
53
+ "decode_bytes_values",
54
+ # State
55
+ "AgentState",
56
+ "RequestState",
57
+ # Session
58
+ "Session",
59
+ "SessionManager",
60
+ "FileSessionManager",
61
+ # Conversation
62
+ "ConversationMessage",
63
+ "ConversationManager",
64
+ "SlidingWindowConversationManager",
65
+ "NullConversationManager",
66
+ ]