mojentic 0.4.2__py3-none-any.whl → 0.5.1__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,28 @@
1
+ from mojentic.llm.gateways.models import LLMMessage
2
+ from mojentic.llm.llm_broker import LLMBroker
3
+ from mojentic.llm.tools.current_datetime import CurrentDateTimeTool
4
+
5
+ # Create an LLM broker with a specified model
6
+ # You can change the model to any supported model
7
+ llm = LLMBroker(model="qwen2.5:7b") # Using the same model as in simple_tool.py
8
+
9
+ # Create our custom tool
10
+ datetime_tool = CurrentDateTimeTool()
11
+
12
+ # Generate a response with tool assistance
13
+ result = llm.generate(
14
+ messages=[LLMMessage(content="What time is it right now? Also, what day of the week is it today?")],
15
+ tools=[datetime_tool]
16
+ )
17
+
18
+ print("LLM Response:")
19
+ print(result)
20
+
21
+ # You can also try with a custom format string
22
+ result = llm.generate(
23
+ messages=[LLMMessage(content="Tell me the current date in a friendly format, like 'Monday, January 1, 2023'")],
24
+ tools=[datetime_tool]
25
+ )
26
+
27
+ print("\nLLM Response with custom format:")
28
+ print(result)
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+
3
+ from mojentic.llm.gateways import OllamaGateway
4
+ from mojentic.llm.gateways.models import LLMMessage
5
+
6
+ llmg = OllamaGateway()
7
+
8
+
9
+ def complete(prompt: str):
10
+ response = llmg.complete(
11
+ model="gemma3:27b",
12
+ messages=[
13
+ LLMMessage(
14
+ content=prompt.strip(),
15
+ image_paths=[
16
+ str(Path.cwd() / "images" / "screen_cap.png")
17
+ ]
18
+ )
19
+ ],
20
+ )
21
+ return response.content
22
+
23
+
24
+ # It's quite good at extracting text from images
25
+ result = complete("Extract the text from the attached image in markdown format.")
26
+ print(result)
27
+
28
+ # It's not bad at extracting and structuring the text it finds in images
29
+ result = complete("""
30
+ Extract the title and text boxes from the attached image in the form of a JSON object.
31
+ Use descriptive key names like `title` or `bullet` or `footer`. In the values, use html
32
+ but don't attempt to convert all of the visual formatting, just simple formatting like
33
+ paragraphs, bold, or italic.
34
+ """)
35
+ print(result)
36
+
37
+ # Gemma3 is not very good at determining hex colours :)
38
+ result = complete("""
39
+ Extract the colours from the attached image in the form of a JSON object, where the key
40
+ is the description of how the colour is used, and the value is the colour in hex format.
41
+ """)
42
+ print(result)
@@ -0,0 +1,58 @@
1
+ from pathlib import Path
2
+ import sys
3
+ import os
4
+
5
+ from mojentic.llm.message_composers import MessageBuilder
6
+
7
+ # Get the project root directory (2 levels up from the current script)
8
+ project_root = Path(os.path.dirname(os.path.abspath(__file__))).parent.parent
9
+
10
+ # Test adding non-existent image
11
+ print("Testing adding non-existent image:")
12
+ non_existent_image = project_root / 'src' / '_examples' / 'images' / 'non_existent_image.jpg'
13
+
14
+ try:
15
+ message_builder = MessageBuilder("Testing non-existent image")
16
+ message_builder.add_image(non_existent_image)
17
+ print("ERROR: Expected FileNotFoundError was not raised")
18
+ sys.exit(1)
19
+ except FileNotFoundError as e:
20
+ print(f"Success: Caught expected exception: {e}")
21
+
22
+ # Test adding non-existent file
23
+ print("\nTesting adding non-existent file:")
24
+ non_existent_file = project_root / 'src' / '_examples' / 'non_existent_file.py'
25
+
26
+ try:
27
+ message_builder = MessageBuilder("Testing non-existent file")
28
+ message_builder.add_file(non_existent_file)
29
+ print("ERROR: Expected FileNotFoundError was not raised")
30
+ sys.exit(1)
31
+ except FileNotFoundError as e:
32
+ print(f"Success: Caught expected exception: {e}")
33
+
34
+ # Test adding existing image
35
+ print("\nTesting adding existing image:")
36
+ existing_image = project_root / 'src' / '_examples' / 'images' / 'xbox-one.jpg'
37
+
38
+ try:
39
+ message_builder = MessageBuilder("Testing existing image")
40
+ message_builder.add_image(existing_image)
41
+ print("Success: Added existing image without exception")
42
+ except FileNotFoundError as e:
43
+ print(f"ERROR: Unexpected exception: {e}")
44
+ sys.exit(1)
45
+
46
+ # Test adding existing file
47
+ print("\nTesting adding existing file:")
48
+ existing_file = Path(__file__) # This file itself (absolute path)
49
+
50
+ try:
51
+ message_builder = MessageBuilder("Testing existing file")
52
+ message_builder.add_file(existing_file)
53
+ print("Success: Added existing file without exception")
54
+ except FileNotFoundError as e:
55
+ print(f"ERROR: Unexpected exception: {e}")
56
+ sys.exit(1)
57
+
58
+ print("\nAll tests passed!")
@@ -0,0 +1,38 @@
1
+ from pathlib import Path
2
+
3
+ from mojentic.llm.message_composers import MessageBuilder
4
+
5
+ # Test de-duplication of images
6
+ print("Testing image de-duplication:")
7
+ image_path = Path.cwd() / 'src' / '_examples' / 'images' / 'xbox-one.jpg'
8
+
9
+ # Create a message builder and add the same image multiple times
10
+ message_builder = MessageBuilder("Testing image de-duplication")
11
+ message_builder.add_image(image_path)
12
+ message_builder.add_image(image_path) # Adding the same image again
13
+ message_builder.add_images(image_path, image_path) # Adding the same image twice more
14
+
15
+ # Build the message and check the number of images
16
+ message = message_builder.build()
17
+ print(f"Number of images in message: {len(message.image_paths)}")
18
+ print(f"Expected number of images: 1")
19
+ print(f"De-duplication working: {len(message.image_paths) == 1}")
20
+
21
+ # Test de-duplication of files
22
+ print("\nTesting file de-duplication:")
23
+ file_path = Path('file_deduplication.py') # This file itself
24
+
25
+ # Create a message builder and add the same file multiple times
26
+ message_builder = MessageBuilder("Testing file de-duplication")
27
+ message_builder.add_file(file_path)
28
+ message_builder.add_file(file_path) # Adding the same file again
29
+ message_builder.add_files(file_path, file_path) # Adding the same file twice more
30
+
31
+ # Build the message and check the number of files
32
+ message = message_builder.build()
33
+ # Since we're using the file content in the message, we need to check file_paths directly
34
+ print(f"Number of files in message_builder.file_paths: {len(message_builder.file_paths)}")
35
+ print(f"Expected number of files: 1")
36
+ print(f"De-duplication working: {len(message_builder.file_paths) == 1}")
37
+
38
+ print("\nTest completed.")
@@ -0,0 +1,14 @@
1
+ from pathlib import Path
2
+
3
+ from mojentic.llm import LLMBroker
4
+ from mojentic.llm.message_composers import MessagesBuilder, MessageBuilder
5
+
6
+ llm = LLMBroker(model="gemma3:27b")
7
+
8
+ message = MessageBuilder("What is in this image?") \
9
+ .add_image(Path.cwd() / 'images' / 'xbox-one.jpg') \
10
+ .build()
11
+
12
+ result = llm.generate(messages=[message])
13
+
14
+ print(result)
@@ -0,0 +1,50 @@
1
+ from pathlib import Path
2
+
3
+ from mojentic.llm import LLMBroker
4
+ from mojentic.llm.message_composers import MessageBuilder
5
+
6
+ # Initialize the LLM broker
7
+ llm = LLMBroker(model="gemma3:27b")
8
+
9
+ # Example 1: Adding multiple specific images using the splat operator
10
+ print("Example 1: Adding multiple specific images")
11
+ message1 = MessageBuilder("What are in these images?") \
12
+ .add_images(
13
+ Path.cwd() / 'images' / 'flash_rom.jpg',
14
+ Path.cwd() / 'images' / 'screen_cap.png' # Assuming this file exists
15
+ ) \
16
+ .build()
17
+
18
+ # Example 2: Adding all JPG images from a directory
19
+ print("\nExample 2: Adding all JPG images from a directory")
20
+ images_dir = Path.cwd() / 'images'
21
+ message2 = MessageBuilder("Describe all these images:") \
22
+ .add_images(images_dir) \
23
+ .build()
24
+
25
+ # Example 3: Using a glob pattern to add specific types of images
26
+ print("\nExample 3: Using a glob pattern")
27
+ message3 = MessageBuilder("What can you tell me about these images?") \
28
+ .add_images(Path.cwd() / 'images' / '*.jpg') \
29
+ .build()
30
+
31
+ # Example 4: Combining different ways to specify images
32
+ print("\nExample 4: Combining different ways to specify images")
33
+ message4 = MessageBuilder("Analyze these images:") \
34
+ .add_images(
35
+ Path.cwd() / 'images' / 'xbox-one.jpg', # Specific image
36
+ images_dir, # All JPGs in directory
37
+ Path.cwd() / 'images' / '*.png' # All PNGs using glob pattern
38
+ ) \
39
+ .build()
40
+
41
+ # Print the number of images added in each example
42
+ print(f"Example 1: Added {len(message1.image_paths)} image(s)")
43
+ print(f"Example 2: Added {len(message2.image_paths)} image(s)")
44
+ print(f"Example 3: Added {len(message3.image_paths)} image(s)")
45
+ print(f"Example 4: Added {len(message4.image_paths)} image(s)")
46
+
47
+ # Generate a response using one of the messages (e.g., message1)
48
+ print("\nGenerating response for Example 1...")
49
+ result = llm.generate(messages=[message1])
50
+ print(result)
_examples/list_models.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
3
  from mojentic.llm.gateways import OllamaGateway, OpenAIGateway
4
+ from mojentic.llm.gateways.anthropic import AnthropicGateway
4
5
 
5
6
  ollama = OllamaGateway()
6
7
  print("Ollama Models:")
@@ -12,4 +13,11 @@ print()
12
13
  openai = OpenAIGateway(os.environ["OPENAI_API_KEY"])
13
14
  print("OpenAI Models:")
14
15
  for model in openai.get_available_models():
15
- print(f"- {model}")
16
+ print(f"- {model}")
17
+
18
+ print()
19
+
20
+ anthropic = AnthropicGateway(os.environ["ANTHROPIC_API_KEY"])
21
+ print("Anthropic Models:")
22
+ for model in anthropic.get_available_models():
23
+ print(f"- {model}")
_examples/raw.py ADDED
@@ -0,0 +1,21 @@
1
+ import os
2
+
3
+ from anthropic import Anthropic
4
+
5
+ client = Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
6
+
7
+ anthropic_args = {
8
+ 'model': "claude-3-5-haiku-20241022",
9
+ 'system': "You are a helpful assistant.",
10
+ 'messages': [
11
+ {
12
+ "role": "user",
13
+ "content": "Say hello world",
14
+ }
15
+ ],
16
+ 'max_tokens': 2000,
17
+ }
18
+
19
+ response = client.messages.create(**anthropic_args)
20
+
21
+ print(response.content[0].text)
mojentic/llm/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
1
  from .llm_broker import LLMBroker
2
2
  from .registry.llm_registry import LLMRegistry
3
3
  from .chat_session import ChatSession
4
+ from .message_composers import MessageBuilder, FileTypeSensor
@@ -0,0 +1,52 @@
1
+ from typing import List
2
+
3
+ import structlog
4
+ from anthropic import Anthropic
5
+
6
+ from mojentic.llm.gateways import LLMGateway
7
+ from mojentic.llm.gateways.models import LLMGatewayResponse, LLMToolCall, MessageRole
8
+ from mojentic.llm.gateways.anthropic_messages_adapter import adapt_messages_to_anthropic
9
+
10
+ logger = structlog.get_logger()
11
+
12
+ class AnthropicGateway(LLMGateway):
13
+ def __init__(self, api_key: str):
14
+ self.client = Anthropic(api_key=api_key)
15
+
16
+ def complete(self, **args) -> LLMGatewayResponse:
17
+
18
+ messages = args.get('messages')
19
+
20
+ system_messages = [m for m in messages if m.role == MessageRole.System]
21
+ user_messages = [m for m in messages if m.role == MessageRole.User]
22
+
23
+ anthropic_args = {
24
+ 'model': args['model'],
25
+ 'system': " " . join([m.content for m in system_messages]) if system_messages else None,
26
+ 'messages': adapt_messages_to_anthropic(user_messages),
27
+ }
28
+
29
+ response = self.client.messages.create(
30
+ **anthropic_args,
31
+ temperature=args.get('temperature', 1.0),
32
+ max_tokens=args.get('num_predict', 2000),
33
+ # thinking={
34
+ # "type": "enabled",
35
+ # "budget_tokens": 32768,
36
+ # }
37
+ )
38
+
39
+ object = None
40
+ tool_calls: List[LLMToolCall] = []
41
+
42
+ return LLMGatewayResponse(
43
+ content=response.content[0].text,
44
+ object=object,
45
+ tool_calls=tool_calls,
46
+ )
47
+
48
+ def get_available_models(self) -> List[str]:
49
+ return sorted([m.id for m in self.client.models.list()])
50
+
51
+ def calculate_embeddings(self, text: str, model: str = "voyage-3-large") -> List[float]:
52
+ raise NotImplementedError("The Anthropic API does not support embedding generation.")
@@ -0,0 +1,72 @@
1
+ import base64
2
+ import json
3
+ import os
4
+ from typing import List, Any
5
+
6
+ import structlog
7
+
8
+ from mojentic.llm.gateways.models import LLMMessage, MessageRole
9
+
10
+ logger = structlog.get_logger()
11
+
12
+
13
+ def adapt_messages_to_anthropic(messages: List[LLMMessage]):
14
+ new_messages: List[dict[str, Any]] = []
15
+ for m in messages:
16
+ if m.role == MessageRole.System:
17
+ # System messages aren't supported by Anthropic
18
+ pass
19
+ elif m.role == MessageRole.User:
20
+ if m.image_paths is not None and len(m.image_paths) > 0:
21
+ # Create a content structure with text and images
22
+ content = []
23
+ if m.content:
24
+ content.append({"type": "text", "text": m.content})
25
+
26
+ # Add each image as a base64-encoded URL
27
+ for image_path in m.image_paths:
28
+ try:
29
+ with open(image_path, "rb") as image_file:
30
+ base64_image = base64.b64encode(image_file.read()).decode('utf-8')
31
+
32
+ # Determine image type from file extension
33
+ _, ext = os.path.splitext(image_path)
34
+ image_type = ext.lstrip('.').lower()
35
+ if image_type not in ['jpeg', 'jpg', 'png', 'gif', 'webp']:
36
+ image_type = 'jpeg' # Default to jpeg if unknown extension
37
+
38
+ content.append({
39
+ "type": "image",
40
+ "source": {
41
+ "type": "base64",
42
+ "media_type": f"image/{image_type}",
43
+ "data": base64_image
44
+ }
45
+ })
46
+ except Exception as e:
47
+ logger.error("Failed to encode image", error=str(e), image_path=image_path)
48
+
49
+ new_messages.append({'role': 'user', 'content': content})
50
+ else:
51
+ new_messages.append({'role': 'user', 'content': m.content})
52
+ elif m.role == MessageRole.Assistant:
53
+ msg = {'role': 'assistant', 'content': m.content or ''}
54
+ # if m.tool_calls is not None:
55
+ # msg['tool_calls'] = [{
56
+ # 'id': m.tool_calls[0].id,
57
+ # 'type': 'function',
58
+ # 'function': {
59
+ # 'name': m.tool_calls[0].name,
60
+ # 'arguments': json.dumps({k: v for k, v in m.tool_calls[0].arguments.items()})
61
+ # }
62
+ # }]
63
+ new_messages.append(msg)
64
+ # elif m.role == MessageRole.Tool:
65
+ # new_messages.append({
66
+ # 'role': 'tool',
67
+ # 'content': m.content,
68
+ # 'tool_call_id': m.tool_calls[0].id
69
+ # })
70
+ else:
71
+ logger.error("Unknown message role", role=m.role)
72
+ return new_messages
@@ -0,0 +1,55 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+
4
+
5
+ class FileGateway:
6
+
7
+ def read(self, path: Union[Path, str]):
8
+ if isinstance(path, str):
9
+ path = Path(path)
10
+ with open(path, 'r') as file:
11
+ return file.read()
12
+
13
+ def exists(self, path: Union[Path, str]) -> bool:
14
+ """
15
+ Check if a file exists.
16
+
17
+ Parameters
18
+ ----------
19
+ path : Union[Path, str]
20
+ Path to the file
21
+
22
+ Returns
23
+ -------
24
+ bool
25
+ True if the file exists, False otherwise
26
+ """
27
+ if isinstance(path, str):
28
+ path = Path(path)
29
+ return path.exists()
30
+
31
+ def is_binary(self, path: Union[Path, str]) -> bool:
32
+ """
33
+ Check if a file is binary.
34
+
35
+ Parameters
36
+ ----------
37
+ path : Union[Path, str]
38
+ Path to the file
39
+
40
+ Returns
41
+ -------
42
+ bool
43
+ True if the file is binary, False otherwise
44
+ """
45
+ if isinstance(path, str):
46
+ path = Path(path)
47
+ try:
48
+ with open(path, 'rb') as file:
49
+ # Read the first 1024 bytes
50
+ chunk = file.read(1024)
51
+ # Check for null bytes, which are typically present in binary files
52
+ return b'\0' in chunk
53
+ except Exception:
54
+ # If there's an error reading the file, assume it's binary
55
+ return True
@@ -21,7 +21,7 @@ class OpenAIGateway(LLMGateway):
21
21
  The OpenAI API key to use.
22
22
  """
23
23
 
24
- def __init__(self, api_key):
24
+ def __init__(self, api_key: str):
25
25
  self.client = OpenAI(api_key=api_key)
26
26
 
27
27
  def complete(self, **args) -> LLMGatewayResponse:
@@ -0,0 +1,317 @@
1
+ from pathlib import Path
2
+ from typing import Union, Dict, List, Optional
3
+
4
+ from mojentic.llm.gateways.file_gateway import FileGateway
5
+ from mojentic.llm.gateways.models import LLMMessage, MessageRole
6
+
7
+
8
+ class FileTypeSensor:
9
+ """Maps file extensions to language declarations for markdown code-fence."""
10
+
11
+ def __init__(self):
12
+ """
13
+ Initialize the TypeSensor with a default mapping of file extensions to language declarations.
14
+
15
+ The TypeSensor is used to determine the appropriate language syntax highlighting
16
+ for code blocks in markdown based on file extensions.
17
+ """
18
+ self.extension_map: Dict[str, str] = {
19
+ 'c': 'c',
20
+ 'cpp': 'c++',
21
+ 'c++': 'c++',
22
+ 'cxx': 'c++',
23
+ 'h': 'c',
24
+ 'objc': 'objective-c',
25
+ 'swift': 'swift',
26
+ 'cs': 'csharp',
27
+ 'fs': 'fsharp',
28
+ 'clj': 'clojure',
29
+ 'py': 'python',
30
+ 'java': 'java',
31
+ 'kt': 'kotlin',
32
+ 'rb': 'ruby',
33
+ 'js': 'javascript',
34
+ 'ts': 'typescript',
35
+ 'rs': 'rust',
36
+ 'go': 'go',
37
+ 'cbl': 'cobol',
38
+ 'html': 'html',
39
+ 'css': 'css',
40
+ 'json': 'json',
41
+ 'csv': 'csv',
42
+ 'xml': 'xml',
43
+ 'md': 'markdown',
44
+ }
45
+ self.default_language = 'text'
46
+
47
+ def add_language(self, extension: str, language: str):
48
+ """
49
+ Add or update a mapping from a file extension to a language declaration.
50
+
51
+ Parameters
52
+ ----------
53
+ extension : str
54
+ The file extension without the leading dot (e.g., 'py', 'js')
55
+ language : str
56
+ The language identifier for markdown code-fence (e.g., 'python', 'javascript')
57
+ """
58
+ self.extension_map[extension] = language
59
+
60
+ def get_language(self, file_path: Path) -> str:
61
+ """
62
+ Determine the language declaration for a file based on its extension.
63
+
64
+ Parameters
65
+ ----------
66
+ file_path : Path
67
+ Path to the file
68
+
69
+ Returns
70
+ -------
71
+ str
72
+ Language declaration for markdown code-fence
73
+ """
74
+ ext = file_path.suffix[1:] # Remove the leading dot
75
+ return self.extension_map.get(ext, self.default_language)
76
+
77
+
78
+ class MessageBuilder():
79
+ """
80
+ A builder class for creating LLM messages with text content, images, and files.
81
+
82
+ This class provides a fluent interface for constructing messages to be sent to an LLM,
83
+ with support for adding text content, images, and file contents. It handles file reading,
84
+ language detection for syntax highlighting, and proper formatting of the message.
85
+ """
86
+ role: MessageRole
87
+ content: Optional[str]
88
+ image_paths: List[Path]
89
+ file_paths: List[Path]
90
+ type_sensor: FileTypeSensor
91
+
92
+ def __init__(self, content: str = None):
93
+ """
94
+ Initialize a new MessageBuilder with optional text content.
95
+
96
+ Parameters
97
+ ----------
98
+ content : str, optional
99
+ Optional text content for the message. If None, the message will only
100
+ contain added files and images.
101
+ """
102
+ self.role = MessageRole.User
103
+ self.content = content
104
+ self.image_paths = []
105
+ self.file_paths = []
106
+ self.type_sensor = FileTypeSensor()
107
+ self.file_gateway = FileGateway()
108
+
109
+ def _file_content_partial(self, file_path: Path) -> str:
110
+ """
111
+ Format the content of a file for inclusion in the message.
112
+
113
+ This method reads the file content and formats it with appropriate markdown code-fence
114
+ syntax highlighting based on the file extension.
115
+
116
+ Parameters
117
+ ----------
118
+ file_path : Path
119
+ Path to the file to be read and formatted
120
+
121
+ Returns
122
+ -------
123
+ str
124
+ Formatted string containing the file path and content with markdown code-fence
125
+ """
126
+ content = self.file_gateway.read(file_path)
127
+ return (f"File: {file_path}\n"
128
+ f"```{self.type_sensor.get_language(file_path)}\n"
129
+ f"{content.strip()}\n"
130
+ f"```\n")
131
+
132
+
133
+ def add_image(self, image_path: Union[str, Path]) -> "MessageBuilder":
134
+ """
135
+ Add a single image to the message.
136
+
137
+ Parameters
138
+ ----------
139
+ image_path : Union[str, Path]
140
+ Path to the image file. Can be a string or Path object.
141
+
142
+ Returns
143
+ -------
144
+ MessageBuilder
145
+ The MessageBuilder instance for method chaining.
146
+
147
+ Raises
148
+ ------
149
+ FileNotFoundError
150
+ If the specified image file does not exist.
151
+ """
152
+ if isinstance(image_path, str):
153
+ image_path = Path(image_path)
154
+ if not self.file_gateway.exists(image_path):
155
+ raise FileNotFoundError(f"Image file not found: {image_path}")
156
+ if image_path not in self.image_paths:
157
+ self.image_paths.append(image_path)
158
+ return self
159
+
160
+ def add_file(self, file_path: Union[str, Path]) -> "MessageBuilder":
161
+ """
162
+ Add a single file to the message.
163
+
164
+ Parameters
165
+ ----------
166
+ file_path : Union[str, Path]
167
+ Path to the file. Can be a string or Path object.
168
+
169
+ Returns
170
+ -------
171
+ MessageBuilder
172
+ The MessageBuilder instance for method chaining.
173
+
174
+ Raises
175
+ ------
176
+ FileNotFoundError
177
+ If the specified file does not exist.
178
+ """
179
+ if isinstance(file_path, str):
180
+ file_path = Path(file_path)
181
+ if not self.file_gateway.exists(file_path):
182
+ raise FileNotFoundError(f"File not found: {file_path}")
183
+ if file_path not in self.file_paths:
184
+ self.file_paths.append(file_path)
185
+ return self
186
+
187
+ def add_images(self, *image_paths: Union[str, Path]) -> "MessageBuilder":
188
+ """
189
+ Add multiple images to the message.
190
+
191
+ Parameters
192
+ ----------
193
+ *image_paths : Union[str, Path]
194
+ Variable number of image paths. Can be strings or Path objects.
195
+ Can include glob patterns like '*.jpg' to include all JPG and PNG images in a directory.
196
+
197
+ Returns
198
+ -------
199
+ MessageBuilder
200
+ The MessageBuilder instance for method chaining.
201
+ """
202
+ for path in image_paths:
203
+ path_obj = Path(path) if isinstance(path, str) else path
204
+
205
+ if path_obj.is_dir():
206
+ for ext in ['*.jpg', '*.png']:
207
+ for img_path in path_obj.glob(ext):
208
+ self.add_image(img_path)
209
+ elif '*' in str(path_obj):
210
+ parent_dir = path_obj.parent if path_obj.parent != Path('.') else Path.cwd()
211
+ for img_path in parent_dir.glob(path_obj.name):
212
+ if img_path.is_file():
213
+ self.add_image(img_path)
214
+ else:
215
+ self.add_image(path_obj)
216
+
217
+ return self
218
+
219
+ def add_files(self, *file_paths: Union[str, Path]) -> "MessageBuilder":
220
+ """
221
+ Add multiple text files to the message, ignoring binary files.
222
+
223
+ Parameters
224
+ ----------
225
+ *file_paths : Union[str, Path]
226
+ Variable number of file paths. Can be strings or Path objects.
227
+ Can include glob patterns like '*.txt' to include all text files in a directory.
228
+ If a directory is provided, all text files in the directory will be added.
229
+
230
+ Returns
231
+ -------
232
+ MessageBuilder
233
+ The MessageBuilder instance for method chaining.
234
+ """
235
+ for path in file_paths:
236
+ path_obj = Path(path) if isinstance(path, str) else path
237
+
238
+ if path_obj.is_dir():
239
+ # If a directory is provided, add all text files in the directory
240
+ for file_path in path_obj.glob('*'):
241
+ if file_path.is_file() and not self.file_gateway.is_binary(file_path):
242
+ self.add_file(file_path)
243
+ elif '*' in str(path_obj):
244
+ # If a glob pattern is provided, add all matching text files
245
+ parent_dir = path_obj.parent if path_obj.parent != Path('.') else Path.cwd()
246
+ for file_path in parent_dir.glob(path_obj.name):
247
+ if file_path.is_file() and not self.file_gateway.is_binary(file_path):
248
+ self.add_file(file_path)
249
+ else:
250
+ # If a single file is provided, add it if it's a text file
251
+ if path_obj.is_file() and not self.file_gateway.is_binary(path_obj):
252
+ self.add_file(path_obj)
253
+
254
+ return self
255
+
256
+ def load_content(self, file_path: Union[str, Path], template_values: Optional[Dict[str, str]] = None) -> "MessageBuilder":
257
+ """
258
+ Load content from a file into the content field of the MessageBuilder.
259
+
260
+ This method reads the content of the specified file and sets it as the content
261
+ of the MessageBuilder, replacing any existing content. If template_values is provided,
262
+ placeholders in the content will be replaced with the corresponding values.
263
+
264
+ Parameters
265
+ ----------
266
+ file_path : Union[str, Path]
267
+ Path to the file to load content from. Can be a string or Path object.
268
+ template_values : Optional[Dict[str, str]], optional
269
+ Dictionary of values used to replace placeholders in the content.
270
+ For example, if the content contains "{greeting}" and template_values is
271
+ {"greeting": "Hello, World!"}, then "{greeting}" will be replaced with
272
+ "Hello, World!". Default is None.
273
+
274
+ Returns
275
+ -------
276
+ MessageBuilder
277
+ The MessageBuilder instance for method chaining.
278
+
279
+ Raises
280
+ ------
281
+ FileNotFoundError
282
+ If the specified file does not exist.
283
+ """
284
+ if isinstance(file_path, str):
285
+ file_path = Path(file_path)
286
+ if not self.file_gateway.exists(file_path):
287
+ raise FileNotFoundError(f"File not found: {file_path}")
288
+ self.content = self.file_gateway.read(file_path)
289
+
290
+ # Replace placeholders with template values if provided
291
+ if template_values:
292
+ for key, value in template_values.items():
293
+ self.content = self.content.replace(f"{{{key}}}", value)
294
+
295
+ return self
296
+
297
+ def build(self) -> LLMMessage:
298
+ """
299
+ Build the final LLMMessage from the accumulated content, images, and files.
300
+
301
+ This method combines all the content, file contents, and image paths into a single
302
+ LLMMessage object that can be sent to an LLM. If files have been added, their contents
303
+ will be formatted and included in the message content.
304
+
305
+ Returns
306
+ -------
307
+ LLMMessage
308
+ An LLMMessage object containing the message content and image paths.
309
+ """
310
+ if self.file_paths:
311
+ file_contents = [self._file_content_partial(p) for p in self.file_paths]
312
+ self.content = "\n\n".join(file_contents)
313
+ return LLMMessage(
314
+ role=self.role,
315
+ content=self.content,
316
+ image_paths=[str(p) for p in self.image_paths]
317
+ )
@@ -0,0 +1,387 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from mojentic.llm.gateways.models import LLMMessage, MessageRole
6
+ from mojentic.llm import MessageBuilder, FileTypeSensor
7
+
8
+
9
+ @pytest.fixture
10
+ def file_gateway(mocker):
11
+ file_gateway = mocker.MagicMock()
12
+ file_gateway.read.return_value = "test file content"
13
+ file_gateway.exists.return_value = True
14
+ file_gateway.is_binary.return_value = False
15
+ return file_gateway
16
+
17
+ @pytest.fixture
18
+ def file_path():
19
+ return Path("/path/to/file.txt")
20
+
21
+ @pytest.fixture
22
+ def whitespace_file_content():
23
+ return "\n\n \n test file content with whitespace \n\n \n"
24
+
25
+
26
+ @pytest.fixture
27
+ def message_builder(file_gateway):
28
+ builder = MessageBuilder()
29
+ builder.file_gateway = file_gateway
30
+ return builder
31
+
32
+
33
+ class DescribeMessageBuilder:
34
+ """
35
+ Specification for the MessageBuilder class which helps build LLM messages.
36
+ """
37
+
38
+ class DescribeInitialization:
39
+ """
40
+ Specifications for MessageBuilder initialization
41
+ """
42
+
43
+ def should_initialize_with_default_values(self):
44
+ """
45
+ Given a new MessageBuilder
46
+ When it is initialized
47
+ Then it should have default values set
48
+ """
49
+ builder = MessageBuilder()
50
+
51
+ assert builder.role == MessageRole.User
52
+ assert builder.content is None
53
+ assert builder.image_paths == []
54
+ assert builder.file_paths == []
55
+ assert isinstance(builder.type_sensor, FileTypeSensor)
56
+ assert isinstance(builder.file_gateway, object)
57
+
58
+ class DescribeBuildMethod:
59
+ """
60
+ Specifications for the build method
61
+ """
62
+
63
+ def should_build_message_with_content(self, message_builder):
64
+ """
65
+ Given a MessageBuilder with content
66
+ When build is called
67
+ Then it should return a message with that content
68
+ """
69
+ message_builder.content = "Test content"
70
+
71
+ message = message_builder.build()
72
+
73
+ assert isinstance(message, LLMMessage)
74
+ assert message.content == "Test content"
75
+ assert message.role == MessageRole.User
76
+ assert message.image_paths == []
77
+
78
+ def should_build_message_with_role(self, message_builder):
79
+ """
80
+ Given a MessageBuilder with a specific role
81
+ When build is called
82
+ Then it should return a message with that role
83
+ """
84
+ message_builder.role = MessageRole.Assistant
85
+
86
+ message = message_builder.build()
87
+
88
+ assert message.role == MessageRole.Assistant
89
+
90
+ def should_build_message_with_image_paths(self, message_builder):
91
+ """
92
+ Given a MessageBuilder with image paths
93
+ When build is called
94
+ Then it should return a message with those image paths
95
+ """
96
+ message_builder.image_paths = [Path("/path/to/image1.jpg"), Path("/path/to/image2.jpg")]
97
+
98
+ message = message_builder.build()
99
+
100
+ assert message.image_paths == ["/path/to/image1.jpg", "/path/to/image2.jpg"]
101
+
102
+ def should_build_message_with_file_content(self, message_builder, file_gateway):
103
+ """
104
+ Given a MessageBuilder with file paths
105
+ When build is called
106
+ Then it should return a message with the file contents
107
+ """
108
+ file_path = Path("/path/to/file.txt")
109
+ message_builder.file_paths = [file_path]
110
+
111
+ message = message_builder.build()
112
+
113
+ file_gateway.read.assert_called_once_with(file_path)
114
+ assert "test file content" in message.content
115
+ assert "File: /path/to/file.txt" in message.content
116
+
117
+ def should_build_message_with_multiple_file_contents(self, message_builder, file_gateway):
118
+ """
119
+ Given a MessageBuilder with multiple file paths
120
+ When build is called
121
+ Then it should return a message with all file contents
122
+ """
123
+ file_path1 = Path("/path/to/file1.txt")
124
+ file_path2 = Path("/path/to/file2.txt")
125
+ message_builder.file_paths = [file_path1, file_path2]
126
+
127
+ message = message_builder.build()
128
+
129
+ assert file_gateway.read.call_count == 2
130
+ assert "File: /path/to/file1.txt" in message.content
131
+ assert "File: /path/to/file2.txt" in message.content
132
+
133
+ class DescribeFileContentPartial:
134
+ """
135
+ Specifications for the _file_content_partial method
136
+ """
137
+
138
+ def should_format_file_content_with_language(self, message_builder, file_gateway, mocker):
139
+ """
140
+ Given a MessageBuilder
141
+ When _file_content_partial is called
142
+ Then it should format the file content with the correct language
143
+ """
144
+ file_path = Path("/path/to/file.py")
145
+ mocker.patch.object(message_builder.type_sensor, 'get_language', return_value='python')
146
+
147
+ result = message_builder._file_content_partial(file_path)
148
+
149
+ file_gateway.read.assert_called_once_with(file_path)
150
+ assert "File: /path/to/file.py" in result
151
+ assert "```python" in result
152
+ assert "test file content" in result
153
+ assert "```" in result
154
+
155
+ def should_strip_whitespace_from_file_content(self, message_builder, file_gateway, file_path, whitespace_file_content, mocker):
156
+ """
157
+ Given a MessageBuilder
158
+ When _file_content_partial is called with content that has whitespace above and below
159
+ Then it should strip the whitespace when putting it in code fences
160
+ """
161
+ # Use the fixtures instead of creating file path and content directly
162
+ file_gateway.read.return_value = whitespace_file_content
163
+ mocker.patch.object(message_builder.type_sensor, 'get_language', return_value='text')
164
+
165
+ result = message_builder._file_content_partial(file_path)
166
+
167
+ file_gateway.read.assert_called_with(file_path)
168
+ assert "File: /path/to/file.txt" in result
169
+ assert "```text" in result
170
+ assert "test file content with whitespace" in result
171
+ assert "```" in result
172
+ # Verify that the content in the code fence doesn't have leading/trailing whitespace
173
+ lines = result.split('\n')
174
+ code_fence_start_index = lines.index("```text")
175
+ code_fence_end_index = lines.index("```", code_fence_start_index + 1)
176
+ code_content = lines[code_fence_start_index + 1:code_fence_end_index]
177
+ assert code_content == ["test file content with whitespace"]
178
+
179
+ class DescribeAddImageMethod:
180
+ """
181
+ Specifications for the add_image method
182
+ """
183
+
184
+ def should_add_image_path_to_list(self, message_builder):
185
+ """
186
+ Given a MessageBuilder
187
+ When add_image is called with a path
188
+ Then it should add the path to the image_paths list
189
+ """
190
+ image_path = Path("/path/to/image.jpg")
191
+
192
+ result = message_builder.add_image(image_path)
193
+
194
+ assert image_path in message_builder.image_paths
195
+ assert result is message_builder # Returns self for method chaining
196
+
197
+ def should_convert_string_path_to_path_object(self, message_builder):
198
+ """
199
+ Given a MessageBuilder
200
+ When add_image is called with a string path
201
+ Then it should convert the string to a Path object
202
+ """
203
+ image_path_str = "/path/to/image.jpg"
204
+
205
+ message_builder.add_image(image_path_str)
206
+
207
+ assert Path(image_path_str) in message_builder.image_paths
208
+
209
+ class DescribeAddImagesMethod:
210
+ """
211
+ Specifications for the add_images method
212
+ """
213
+
214
+ def should_add_multiple_specific_images(self, message_builder):
215
+ """
216
+ Given a MessageBuilder
217
+ When add_images is called with multiple specific image paths
218
+ Then it should add all paths to the image_paths list
219
+ """
220
+ image_path1 = Path("/path/to/image1.jpg")
221
+ image_path2 = Path("/path/to/image2.jpg")
222
+
223
+ result = message_builder.add_images(image_path1, image_path2)
224
+
225
+ assert image_path1 in message_builder.image_paths
226
+ assert image_path2 in message_builder.image_paths
227
+ assert result is message_builder # Returns self for method chaining
228
+
229
+ def should_add_all_jpg_images_from_directory(self, message_builder, mocker):
230
+ """
231
+ Given a MessageBuilder
232
+ When add_images is called with a directory path
233
+ Then it should add all JPG images in that directory
234
+ """
235
+ dir_path = Path("/path/to/images")
236
+ jpg_files = [Path("/path/to/images/image1.jpg"), Path("/path/to/images/image2.jpg")]
237
+
238
+ # Mock is_dir, glob, and exists methods
239
+ mocker.patch.object(Path, 'is_dir', return_value=True)
240
+ mocker.patch.object(Path, 'glob', return_value=jpg_files)
241
+ mocker.patch.object(Path, 'exists', return_value=True)
242
+
243
+ message_builder.add_images(dir_path)
244
+
245
+ assert jpg_files[0] in message_builder.image_paths
246
+ assert jpg_files[1] in message_builder.image_paths
247
+
248
+ def should_add_images_matching_glob_pattern(self, message_builder, mocker):
249
+ """
250
+ Given a MessageBuilder
251
+ When add_images is called with a path containing a wildcard
252
+ Then it should add all matching files
253
+ """
254
+ pattern_path = Path("/path/to/*.jpg")
255
+ matching_files = [Path("/path/to/image1.jpg"), Path("/path/to/image2.jpg")]
256
+
257
+ # Mock methods
258
+ mocker.patch.object(Path, 'is_dir', return_value=False)
259
+ mocker.patch.object(Path, 'glob', return_value=matching_files)
260
+ mocker.patch.object(Path, 'is_file', return_value=True)
261
+ mocker.patch.object(Path, 'exists', return_value=True)
262
+
263
+ # Mock the parent property and its glob method
264
+ parent_mock = mocker.MagicMock()
265
+ parent_mock.glob.return_value = matching_files
266
+ mocker.patch.object(Path, 'parent', parent_mock)
267
+
268
+ message_builder.add_images(pattern_path)
269
+
270
+ assert matching_files[0] in message_builder.image_paths
271
+ assert matching_files[1] in message_builder.image_paths
272
+
273
+ class DescribeLoadContentMethod:
274
+ """
275
+ Specifications for the load_content method
276
+ """
277
+
278
+ def should_load_content_from_file(self, message_builder, file_gateway, file_path):
279
+ """
280
+ Given a MessageBuilder
281
+ When load_content is called with a file path
282
+ Then it should load the content from the file and set it as the content
283
+ """
284
+ result = message_builder.load_content(file_path)
285
+
286
+ file_gateway.read.assert_called_once_with(file_path)
287
+ assert message_builder.content == "test file content"
288
+ assert result is message_builder # Returns self for method chaining
289
+
290
+ def should_convert_string_path_to_path_object(self, message_builder, file_gateway):
291
+ """
292
+ Given a MessageBuilder
293
+ When load_content is called with a string path
294
+ Then it should convert the string to a Path object
295
+ """
296
+ file_path_str = "/path/to/file.txt"
297
+
298
+ message_builder.load_content(file_path_str)
299
+
300
+ file_gateway.read.assert_called_once_with(Path(file_path_str))
301
+
302
+ def should_raise_error_if_file_not_found(self, message_builder, file_gateway, file_path):
303
+ """
304
+ Given a MessageBuilder
305
+ When load_content is called with a non-existent file
306
+ Then it should raise a FileNotFoundError
307
+ """
308
+ file_gateway.exists.return_value = False
309
+
310
+ with pytest.raises(FileNotFoundError):
311
+ message_builder.load_content(file_path)
312
+
313
+ def should_replace_placeholders_with_template_values(self, message_builder, file_gateway, file_path):
314
+ """
315
+ Given a MessageBuilder
316
+ When load_content is called with a file path and template values
317
+ Then it should replace placeholders in the content with the corresponding values
318
+ """
319
+ # Set up the file content with placeholders
320
+ file_gateway.read.return_value = "Hello, {name}! Today is {day}."
321
+
322
+ # Call load_content with template values
323
+ template_values = {"name": "World", "day": "Monday"}
324
+ result = message_builder.load_content(file_path, template_values)
325
+
326
+ # Verify that placeholders were replaced
327
+ assert message_builder.content == "Hello, World! Today is Monday."
328
+ assert result is message_builder # Returns self for method chaining
329
+
330
+
331
+ class DescribeTypeSensor:
332
+ """
333
+ Specification for the TypeSensor class which maps file extensions to language declarations.
334
+ """
335
+
336
+ def should_initialize_with_default_mappings(self):
337
+ """
338
+ Given a new TypeSensor
339
+ When it is initialized
340
+ Then it should have default mappings
341
+ """
342
+ sensor = FileTypeSensor()
343
+
344
+ assert sensor.extension_map['py'] == 'python'
345
+ assert sensor.extension_map['js'] == 'javascript'
346
+ assert sensor.default_language == 'text'
347
+
348
+ def should_get_language_for_known_extension(self):
349
+ """
350
+ Given a TypeSensor
351
+ When get_language is called with a known extension
352
+ Then it should return the correct language
353
+ """
354
+ sensor = FileTypeSensor()
355
+ file_path = Path("/path/to/file.py")
356
+
357
+ language = sensor.get_language(file_path)
358
+
359
+ assert language == 'python'
360
+
361
+ def should_get_default_language_for_unknown_extension(self):
362
+ """
363
+ Given a TypeSensor
364
+ When get_language is called with an unknown extension
365
+ Then it should return the default language
366
+ """
367
+ sensor = FileTypeSensor()
368
+ file_path = Path("/path/to/file.unknown")
369
+
370
+ language = sensor.get_language(file_path)
371
+
372
+ assert language == 'text'
373
+
374
+ def should_add_new_language_mapping(self):
375
+ """
376
+ Given a TypeSensor
377
+ When add_language is called
378
+ Then it should add the new mapping
379
+ """
380
+ sensor = FileTypeSensor()
381
+ sensor.add_language('custom', 'customlang')
382
+
383
+ assert sensor.extension_map['custom'] == 'customlang'
384
+
385
+ file_path = Path("/path/to/file.custom")
386
+ language = sensor.get_language(file_path)
387
+ assert language == 'customlang'
@@ -0,0 +1,47 @@
1
+ from datetime import datetime
2
+ from mojentic.llm.tools.llm_tool import LLMTool
3
+
4
+
5
+ class CurrentDateTimeTool(LLMTool):
6
+ def run(self, format_string: str = "%Y-%m-%d %H:%M:%S") -> dict:
7
+ """
8
+ Returns the current date and time.
9
+
10
+ Parameters
11
+ ----------
12
+ format_string : str, optional
13
+ The format string for the datetime, by default "%Y-%m-%d %H:%M:%S"
14
+
15
+ Returns
16
+ -------
17
+ dict
18
+ A dictionary containing the current date and time
19
+ """
20
+ current_time = datetime.now()
21
+ formatted_time = current_time.strftime(format_string)
22
+
23
+ return {
24
+ "current_datetime": formatted_time,
25
+ "timestamp": current_time.timestamp(),
26
+ "timezone": datetime.now().astimezone().tzname()
27
+ }
28
+
29
+ @property
30
+ def descriptor(self):
31
+ return {
32
+ "type": "function",
33
+ "function": {
34
+ "name": "get_current_datetime",
35
+ "description": "Get the current date and time. Useful when you need to know the current time or date.",
36
+ "parameters": {
37
+ "type": "object",
38
+ "properties": {
39
+ "format_string": {
40
+ "type": "string",
41
+ "description": "Format string for the datetime (e.g., '%Y-%m-%d %H:%M:%S', '%A, %B %d, %Y'). Default is ISO format."
42
+ }
43
+ },
44
+ "required": []
45
+ }
46
+ }
47
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mojentic
3
- Version: 0.4.2
3
+ Version: 0.5.1
4
4
  Summary: Mojentic is an agentic framework that aims to provide a simple and flexible way to assemble teams of agents to solve complex problems.
5
5
  Author-email: Stacey Vetzal <stacey@vetzal.com>
6
6
  Project-URL: Homepage, https://github.com/mojility/mojentic
@@ -15,6 +15,7 @@ Requires-Dist: pydantic
15
15
  Requires-Dist: structlog
16
16
  Requires-Dist: ollama
17
17
  Requires-Dist: openai
18
+ Requires-Dist: anthropic
18
19
  Requires-Dist: tiktoken
19
20
  Requires-Dist: parsedatetime
20
21
  Requires-Dist: pytz
@@ -6,11 +6,18 @@ _examples/characterize_openai.py,sha256=JQQNjsHIEegjLAS3uzDmq_Ci_ZqXTqjcoVaC-rS_
6
6
  _examples/chat_session.py,sha256=7mLpH4IGQUgyyiY-fYbikDEiR5vEH-V5Z1qtB50d_qg,288
7
7
  _examples/chat_session_with_tool.py,sha256=vQfwR24ze5LIhn0MQTQr-JMVGi4Df_SgMXi1npKBt-g,376
8
8
  _examples/coding_file_tool.py,sha256=P4nCuFJ2UW0B_S7N03ImBNs-ADzBC7gb3ivCE0IJAqw,2570
9
+ _examples/current_datetime_tool_example.py,sha256=Ihjwp9svf2rf4WUGOzs6L_rEqc5QhPfA6E9J4MGoGCQ,919
10
+ _examples/design_analysis.py,sha256=66JxvnA065QV__WXpxJX_eTBDan34nGcKvao75UPLlw,1321
9
11
  _examples/embeddings.py,sha256=94DAMsMB35BU98hOfOeOsbqntcequFSdtaStox2hTvk,267
12
+ _examples/ensures_files_exist.py,sha256=LAwX9YtUZGI6NT254PY7oC5yg6q-b9Q_Dez9gGrabQM,2009
13
+ _examples/file_deduplication.py,sha256=jYayx4BCrk1uJwVBkjIXzguLQOWLREi4PBPzoK9LuOU,1665
10
14
  _examples/file_tool.py,sha256=F8xU7JdTScNzEXyGYqK6D_xQ28MbUXXm854WJmJkZo8,2214
11
15
  _examples/image_analysis.py,sha256=Kj49vLQD1DIpvv5P7rir4BqzsVCTgq-tfppqXcYVJkA,503
16
+ _examples/image_broker.py,sha256=SoIphNj98zk1i7EQc7M2n2O_9ShDHwErmJ6jf67mzX0,355
17
+ _examples/image_broker_splat.py,sha256=O7rzTFUka32if4G4VuXvhu1O-2lRMWfi0r8gjIE8-0Y,1934
12
18
  _examples/iterative_solver.py,sha256=ANGdC74ymHosVt6xUBjplkJl_W3ALTGxOkDpPLEDcm8,1331
13
- _examples/list_models.py,sha256=-QJzlxsOfwGQCGBDEbSOeUXA9CegA6KWnZu_TYzgstU,346
19
+ _examples/list_models.py,sha256=8noMpGeXOdX5Pf0NXCt_CRurOKEg_5luhWveGntBhe8,578
20
+ _examples/raw.py,sha256=Y2wvgynFuoUs28agE4ijsLYec8VRjiReklqlCH2lERs,442
14
21
  _examples/react.py,sha256=VQ-5MmjUXoHzBFPTV_JrocuOkDzZ8oyUUSYLlEToJ_0,939
15
22
  _examples/recursive_agent.py,sha256=NHmX8iQc1y43AKh1ZlAdV9OZ18qVaRRmSaKALE_KI6k,2966
16
23
  _examples/routed_send_response.py,sha256=FHhTy7y2f7GY_WAWZYqOnFqF1xeJTBuS5EjfJGsJ3S8,4146
@@ -47,19 +54,24 @@ mojentic/audit/event_store.py,sha256=uw5ploNym9CvI-WDeW3Y4qHJupPWBQaGrfrubxOfmpA
47
54
  mojentic/audit/event_store_spec.py,sha256=mCryZGWNXP9CmukvD3hUyQGs_7afjwbTK3RdT831Lzc,594
48
55
  mojentic/context/__init__.py,sha256=MKMP7ViQg8gMtLFvn9pf47XMc5beA5Wx95K4dEw93z8,55
49
56
  mojentic/context/shared_working_memory.py,sha256=Zt9MNGErEkDIUAaHvyhEOiTaEobI9l0MV4Z59lQFBr0,396
50
- mojentic/llm/__init__.py,sha256=EL2BRdSTAmPnv8MXOY0gQxRjNxs-4MvyJTGxNSd9iek,119
57
+ mojentic/llm/__init__.py,sha256=mwdPpTRofw7_KSlW6ZQmcM-GSpyISWyI2bxphKpV7A0,180
51
58
  mojentic/llm/chat_session.py,sha256=H2gY0mZYVym8jC69VHsmKaRZ9T87Suyw0-TW5r850nA,3992
52
59
  mojentic/llm/chat_session_spec.py,sha256=8-jj-EHV2WwWuvo3t8I75kSEAYiG1nR-OEwkkLTi_z0,3872
53
60
  mojentic/llm/llm_broker.py,sha256=8dEgqU-cPesPk4-jx36oPnvKxB34bLONbpyAW_2L-es,5884
54
61
  mojentic/llm/llm_broker_spec.py,sha256=zxOf3n8lXmUsowzlMcC-fRcOj_6wwDf7xIqLIV278nM,6979
62
+ mojentic/llm/message_composers.py,sha256=IrTK6HrwAQtJRMW5spMRZmX4yKHl2SJdOkFYvhVXZYg,11305
63
+ mojentic/llm/message_composers_spec.py,sha256=IXZ-Gl--1MGbmRkFNxucWTE7OZgLQHDDgw1WqMiZnF0,14762
55
64
  mojentic/llm/gateways/__init__.py,sha256=u7hXzngoRw_qbsJeiCH2NQ8vC2hF5DnqcXsfLVVPSSw,104
65
+ mojentic/llm/gateways/anthropic.py,sha256=SsyNjq9QaXaqiMM43C9fwLp57hpgFtwNPJUnOAYVrtc,1788
66
+ mojentic/llm/gateways/anthropic_messages_adapter.py,sha256=K6kEZeVt7E1endbGMLsh5l9SxC3Y5dnvbcejVqi_qUs,3003
56
67
  mojentic/llm/gateways/embeddings_gateway.py,sha256=kcOhiyHzOyQgKgwPDQJD5oVvfwk71GsBgMYJOIDv5NU,1347
68
+ mojentic/llm/gateways/file_gateway.py,sha256=3bZpalSyl_R4016WzCmmjUBDtAgPsmx19eVGv6p1Ufk,1418
57
69
  mojentic/llm/gateways/llm_gateway.py,sha256=5BayP6VuMgMHdAzCFaXLRjRCWh-IOYnaq_s4LZ0_3x4,2559
58
70
  mojentic/llm/gateways/models.py,sha256=rHoXkaK5FOgWV5X-nkFhqubwk3sUQceJj3rBnxgqB-E,2335
59
71
  mojentic/llm/gateways/ollama.py,sha256=629fpZhC0zVCYqj360-PKTT4mQOLec5nzzvfMtS_mLQ,7581
60
72
  mojentic/llm/gateways/ollama_messages_adapter.py,sha256=kUN_p2FyN88_trXMcL-Xsn9xPBU7pGKlJwTUEUCf6G4,1404
61
73
  mojentic/llm/gateways/ollama_messages_adapter_spec.py,sha256=gVRbWDrHOa1EiZ0CkEWe0pGn-GKRqdGb-x56HBQeYSE,4981
62
- mojentic/llm/gateways/openai.py,sha256=Jex-11SnzK58E1dHbNV8IVowY3UlhBbfl_YsEOTXSYA,4468
74
+ mojentic/llm/gateways/openai.py,sha256=lYD2-DWt2W3fnmO7k8m29PD-wx4UDvcpPLduanj7tzc,4473
63
75
  mojentic/llm/gateways/openai_message_adapter_spec.py,sha256=IgXyio15nXn11MiFpUnAxm5fFpPU9m-c4qerd9gbpBA,6473
64
76
  mojentic/llm/gateways/openai_messages_adapter.py,sha256=Qwidv7C-wXDOEV8NYNjgIbHnJSlkcoK10FaeLos5zTc,2882
65
77
  mojentic/llm/gateways/tokenizer_gateway.py,sha256=ztuqfunlJ6xmyUPPHcC_69-kegiNJD6jdSEde7hDh2w,485
@@ -69,6 +81,7 @@ mojentic/llm/registry/models.py,sha256=XOZ0ElTL5mEicpcUk9lrVr0GRFgR1uFmDm7GGWm7y
69
81
  mojentic/llm/registry/populate_registry_from_ollama.py,sha256=YFlnyXW-Htveu8frPEntV-c_84Xoh_WhHGww5IqtPIg,2539
70
82
  mojentic/llm/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
83
  mojentic/llm/tools/ask_user_tool.py,sha256=WrI3uTzIEmr_S5Pl-rmvh2f3O2IIFYfnJ5zeel_59ck,972
84
+ mojentic/llm/tools/current_datetime.py,sha256=JkIFlBBnvE4CMcgZizqITVTtNfL7zeeLxD54E7oMeC8,1585
72
85
  mojentic/llm/tools/date_resolver.py,sha256=Vs8kmAONKif03YcVMZ3TqkKyOJZiy1yEAOqjTcvJFA8,2048
73
86
  mojentic/llm/tools/date_resolver_spec.py,sha256=OaRvJyhSN8sgi75euk4E5ImaqUmvQdgZKY8u_NOiPWE,1185
74
87
  mojentic/llm/tools/file_manager.py,sha256=X8Uw4XtdH_Ol2EB3SN11-GYlC1diJ1cAywU9_EuCjCg,3788
@@ -78,8 +91,8 @@ mojentic/llm/tools/tool_wrapper_spec.py,sha256=LGqtre-g8SzOy3xtpbMdgTnw2EdYutmFO
78
91
  mojentic/llm/tools/web_search.py,sha256=L1accST2xMhGAkwHCLlIvKihTLiaYxl0NI6IqCJWGCw,1102
79
92
  mojentic/utils/__init__.py,sha256=lqECkkoFvHFttDnafRE1vvh0Dmna_lwupMToP5VvX5k,115
80
93
  mojentic/utils/formatting.py,sha256=bPrwwdluXdQ8TsFxfWtHNOeMWKNvAfABSoUnnA1g7c8,947
81
- mojentic-0.4.2.dist-info/licenses/LICENSE.md,sha256=txSgV8n5zY1W3NiF5HHsCwlaW0e8We1cSC6TuJUqxXA,1060
82
- mojentic-0.4.2.dist-info/METADATA,sha256=ObeHqe_V8iAVsxYDl02IUxluaHTGpGMRAaW3aS6bb5U,4910
83
- mojentic-0.4.2.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
84
- mojentic-0.4.2.dist-info/top_level.txt,sha256=Q-BvPQ8Eu1jnEqK8Xkr6A9C8Xa1z38oPZRHuA5MCTqg,19
85
- mojentic-0.4.2.dist-info/RECORD,,
94
+ mojentic-0.5.1.dist-info/licenses/LICENSE.md,sha256=txSgV8n5zY1W3NiF5HHsCwlaW0e8We1cSC6TuJUqxXA,1060
95
+ mojentic-0.5.1.dist-info/METADATA,sha256=YIXg6guNrmTF3fBI225OEvBGuFro2uPOdmnGl5xi1Xc,4935
96
+ mojentic-0.5.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
97
+ mojentic-0.5.1.dist-info/top_level.txt,sha256=Q-BvPQ8Eu1jnEqK8Xkr6A9C8Xa1z38oPZRHuA5MCTqg,19
98
+ mojentic-0.5.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (77.0.3)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5