mojentic 0.4.1__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -163,14 +163,14 @@ class OllamaGateway(LLMGateway):
163
163
 
164
164
  def get_available_models(self) -> List[str]:
165
165
  """
166
- Get the list of available local models.
166
+ Get the list of available local models, sorted alphabetically.
167
167
 
168
168
  Returns
169
169
  -------
170
170
  List[str]
171
- The list of available models.
171
+ The list of available models, sorted alphabetically.
172
172
  """
173
- return [m.model for m in self.client.list().models]
173
+ return sorted([m.model for m in self.client.list().models])
174
174
 
175
175
  def pull_model(self, model: str) -> None:
176
176
  """
@@ -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:
@@ -94,7 +94,15 @@ class OpenAIGateway(LLMGateway):
94
94
  )
95
95
 
96
96
  def get_available_models(self) -> list[str]:
97
- return [m.id for m in self.client.models.list()]
97
+ """
98
+ Get the list of available OpenAI models, sorted alphabetically.
99
+
100
+ Returns
101
+ -------
102
+ list[str]
103
+ The list of available models, sorted alphabetically.
104
+ """
105
+ return sorted([m.id for m in self.client.models.list()])
98
106
 
99
107
  def calculate_embeddings(self, text: str, model: str = "text-embedding-3-large") -> List[float]:
100
108
  """
@@ -0,0 +1,276 @@
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}\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 build(self) -> LLMMessage:
257
+ """
258
+ Build the final LLMMessage from the accumulated content, images, and files.
259
+
260
+ This method combines all the content, file contents, and image paths into a single
261
+ LLMMessage object that can be sent to an LLM. If files have been added, their contents
262
+ will be formatted and included in the message content.
263
+
264
+ Returns
265
+ -------
266
+ LLMMessage
267
+ An LLMMessage object containing the message content and image paths.
268
+ """
269
+ if self.file_paths:
270
+ file_contents = [self._file_content_partial(p) for p in self.file_paths]
271
+ self.content = "\n\n".join(file_contents)
272
+ return LLMMessage(
273
+ role=self.role,
274
+ content=self.content,
275
+ image_paths=[str(p) for p in self.image_paths]
276
+ )
@@ -0,0 +1,298 @@
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
+
18
+ @pytest.fixture
19
+ def message_builder(file_gateway):
20
+ builder = MessageBuilder()
21
+ builder.file_gateway = file_gateway
22
+ return builder
23
+
24
+
25
+ class DescribeMessageBuilder:
26
+ """
27
+ Specification for the MessageBuilder class which helps build LLM messages.
28
+ """
29
+
30
+ class DescribeInitialization:
31
+ """
32
+ Specifications for MessageBuilder initialization
33
+ """
34
+
35
+ def should_initialize_with_default_values(self):
36
+ """
37
+ Given a new MessageBuilder
38
+ When it is initialized
39
+ Then it should have default values set
40
+ """
41
+ builder = MessageBuilder()
42
+
43
+ assert builder.role == MessageRole.User
44
+ assert builder.content is None
45
+ assert builder.image_paths == []
46
+ assert builder.file_paths == []
47
+ assert isinstance(builder.type_sensor, FileTypeSensor)
48
+ assert isinstance(builder.file_gateway, object)
49
+
50
+ class DescribeBuildMethod:
51
+ """
52
+ Specifications for the build method
53
+ """
54
+
55
+ def should_build_message_with_content(self, message_builder):
56
+ """
57
+ Given a MessageBuilder with content
58
+ When build is called
59
+ Then it should return a message with that content
60
+ """
61
+ message_builder.content = "Test content"
62
+
63
+ message = message_builder.build()
64
+
65
+ assert isinstance(message, LLMMessage)
66
+ assert message.content == "Test content"
67
+ assert message.role == MessageRole.User
68
+ assert message.image_paths == []
69
+
70
+ def should_build_message_with_role(self, message_builder):
71
+ """
72
+ Given a MessageBuilder with a specific role
73
+ When build is called
74
+ Then it should return a message with that role
75
+ """
76
+ message_builder.role = MessageRole.Assistant
77
+
78
+ message = message_builder.build()
79
+
80
+ assert message.role == MessageRole.Assistant
81
+
82
+ def should_build_message_with_image_paths(self, message_builder):
83
+ """
84
+ Given a MessageBuilder with image paths
85
+ When build is called
86
+ Then it should return a message with those image paths
87
+ """
88
+ message_builder.image_paths = [Path("/path/to/image1.jpg"), Path("/path/to/image2.jpg")]
89
+
90
+ message = message_builder.build()
91
+
92
+ assert message.image_paths == ["/path/to/image1.jpg", "/path/to/image2.jpg"]
93
+
94
+ def should_build_message_with_file_content(self, message_builder, file_gateway):
95
+ """
96
+ Given a MessageBuilder with file paths
97
+ When build is called
98
+ Then it should return a message with the file contents
99
+ """
100
+ file_path = Path("/path/to/file.txt")
101
+ message_builder.file_paths = [file_path]
102
+
103
+ message = message_builder.build()
104
+
105
+ file_gateway.read.assert_called_once_with(file_path)
106
+ assert "test file content" in message.content
107
+ assert "File: /path/to/file.txt" in message.content
108
+
109
+ def should_build_message_with_multiple_file_contents(self, message_builder, file_gateway):
110
+ """
111
+ Given a MessageBuilder with multiple file paths
112
+ When build is called
113
+ Then it should return a message with all file contents
114
+ """
115
+ file_path1 = Path("/path/to/file1.txt")
116
+ file_path2 = Path("/path/to/file2.txt")
117
+ message_builder.file_paths = [file_path1, file_path2]
118
+
119
+ message = message_builder.build()
120
+
121
+ assert file_gateway.read.call_count == 2
122
+ assert "File: /path/to/file1.txt" in message.content
123
+ assert "File: /path/to/file2.txt" in message.content
124
+
125
+ class DescribeFileContentPartial:
126
+ """
127
+ Specifications for the _file_content_partial method
128
+ """
129
+
130
+ def should_format_file_content_with_language(self, message_builder, file_gateway, mocker):
131
+ """
132
+ Given a MessageBuilder
133
+ When _file_content_partial is called
134
+ Then it should format the file content with the correct language
135
+ """
136
+ file_path = Path("/path/to/file.py")
137
+ mocker.patch.object(message_builder.type_sensor, 'get_language', return_value='python')
138
+
139
+ result = message_builder._file_content_partial(file_path)
140
+
141
+ file_gateway.read.assert_called_once_with(file_path)
142
+ assert "File: /path/to/file.py" in result
143
+ assert "```python" in result
144
+ assert "test file content" in result
145
+ assert "```" in result
146
+
147
+ class DescribeAddImageMethod:
148
+ """
149
+ Specifications for the add_image method
150
+ """
151
+
152
+ def should_add_image_path_to_list(self, message_builder):
153
+ """
154
+ Given a MessageBuilder
155
+ When add_image is called with a path
156
+ Then it should add the path to the image_paths list
157
+ """
158
+ image_path = Path("/path/to/image.jpg")
159
+
160
+ result = message_builder.add_image(image_path)
161
+
162
+ assert image_path in message_builder.image_paths
163
+ assert result is message_builder # Returns self for method chaining
164
+
165
+ def should_convert_string_path_to_path_object(self, message_builder):
166
+ """
167
+ Given a MessageBuilder
168
+ When add_image is called with a string path
169
+ Then it should convert the string to a Path object
170
+ """
171
+ image_path_str = "/path/to/image.jpg"
172
+
173
+ message_builder.add_image(image_path_str)
174
+
175
+ assert Path(image_path_str) in message_builder.image_paths
176
+
177
+ class DescribeAddImagesMethod:
178
+ """
179
+ Specifications for the add_images method
180
+ """
181
+
182
+ def should_add_multiple_specific_images(self, message_builder):
183
+ """
184
+ Given a MessageBuilder
185
+ When add_images is called with multiple specific image paths
186
+ Then it should add all paths to the image_paths list
187
+ """
188
+ image_path1 = Path("/path/to/image1.jpg")
189
+ image_path2 = Path("/path/to/image2.jpg")
190
+
191
+ result = message_builder.add_images(image_path1, image_path2)
192
+
193
+ assert image_path1 in message_builder.image_paths
194
+ assert image_path2 in message_builder.image_paths
195
+ assert result is message_builder # Returns self for method chaining
196
+
197
+ def should_add_all_jpg_images_from_directory(self, message_builder, mocker):
198
+ """
199
+ Given a MessageBuilder
200
+ When add_images is called with a directory path
201
+ Then it should add all JPG images in that directory
202
+ """
203
+ dir_path = Path("/path/to/images")
204
+ jpg_files = [Path("/path/to/images/image1.jpg"), Path("/path/to/images/image2.jpg")]
205
+
206
+ # Mock is_dir, glob, and exists methods
207
+ mocker.patch.object(Path, 'is_dir', return_value=True)
208
+ mocker.patch.object(Path, 'glob', return_value=jpg_files)
209
+ mocker.patch.object(Path, 'exists', return_value=True)
210
+
211
+ message_builder.add_images(dir_path)
212
+
213
+ assert jpg_files[0] in message_builder.image_paths
214
+ assert jpg_files[1] in message_builder.image_paths
215
+
216
+ def should_add_images_matching_glob_pattern(self, message_builder, mocker):
217
+ """
218
+ Given a MessageBuilder
219
+ When add_images is called with a path containing a wildcard
220
+ Then it should add all matching files
221
+ """
222
+ pattern_path = Path("/path/to/*.jpg")
223
+ matching_files = [Path("/path/to/image1.jpg"), Path("/path/to/image2.jpg")]
224
+
225
+ # Mock methods
226
+ mocker.patch.object(Path, 'is_dir', return_value=False)
227
+ mocker.patch.object(Path, 'glob', return_value=matching_files)
228
+ mocker.patch.object(Path, 'is_file', return_value=True)
229
+ mocker.patch.object(Path, 'exists', return_value=True)
230
+
231
+ # Mock the parent property and its glob method
232
+ parent_mock = mocker.MagicMock()
233
+ parent_mock.glob.return_value = matching_files
234
+ mocker.patch.object(Path, 'parent', parent_mock)
235
+
236
+ message_builder.add_images(pattern_path)
237
+
238
+ assert matching_files[0] in message_builder.image_paths
239
+ assert matching_files[1] in message_builder.image_paths
240
+
241
+
242
+ class DescribeTypeSensor:
243
+ """
244
+ Specification for the TypeSensor class which maps file extensions to language declarations.
245
+ """
246
+
247
+ def should_initialize_with_default_mappings(self):
248
+ """
249
+ Given a new TypeSensor
250
+ When it is initialized
251
+ Then it should have default mappings
252
+ """
253
+ sensor = FileTypeSensor()
254
+
255
+ assert sensor.extension_map['py'] == 'python'
256
+ assert sensor.extension_map['js'] == 'javascript'
257
+ assert sensor.default_language == 'text'
258
+
259
+ def should_get_language_for_known_extension(self):
260
+ """
261
+ Given a TypeSensor
262
+ When get_language is called with a known extension
263
+ Then it should return the correct language
264
+ """
265
+ sensor = FileTypeSensor()
266
+ file_path = Path("/path/to/file.py")
267
+
268
+ language = sensor.get_language(file_path)
269
+
270
+ assert language == 'python'
271
+
272
+ def should_get_default_language_for_unknown_extension(self):
273
+ """
274
+ Given a TypeSensor
275
+ When get_language is called with an unknown extension
276
+ Then it should return the default language
277
+ """
278
+ sensor = FileTypeSensor()
279
+ file_path = Path("/path/to/file.unknown")
280
+
281
+ language = sensor.get_language(file_path)
282
+
283
+ assert language == 'text'
284
+
285
+ def should_add_new_language_mapping(self):
286
+ """
287
+ Given a TypeSensor
288
+ When add_language is called
289
+ Then it should add the new mapping
290
+ """
291
+ sensor = FileTypeSensor()
292
+ sensor.add_language('custom', 'customlang')
293
+
294
+ assert sensor.extension_map['custom'] == 'customlang'
295
+
296
+ file_path = Path("/path/to/file.custom")
297
+ language = sensor.get_language(file_path)
298
+ 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.1
3
+ Version: 0.5.0
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=zL7OjYNcim1XXARFBxh3ym4fXOv8qSGCd_pSoOkceLQ,9565
63
+ mojentic/llm/message_composers_spec.py,sha256=iEUlNafRVVb5k60BomMpK1JvIHsaCUD6vj_pypVUBaY,10699
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
- mojentic/llm/gateways/ollama.py,sha256=bo49Z5KkU-AeF3a5rkf4T9Tan1KyOMuDmhvm0nF1Y4E,7527
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=iCygOTxxp9-KFYdxXOOGQmEmCigQeZ-IHHiw0eiiyMI,4248
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.1.dist-info/licenses/LICENSE.md,sha256=txSgV8n5zY1W3NiF5HHsCwlaW0e8We1cSC6TuJUqxXA,1060
82
- mojentic-0.4.1.dist-info/METADATA,sha256=muHTN1oH7T7xmh8qHIqlQnqfF4MOCZUywwt4MpbEmjY,4910
83
- mojentic-0.4.1.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
84
- mojentic-0.4.1.dist-info/top_level.txt,sha256=Q-BvPQ8Eu1jnEqK8Xkr6A9C8Xa1z38oPZRHuA5MCTqg,19
85
- mojentic-0.4.1.dist-info/RECORD,,
94
+ mojentic-0.5.0.dist-info/licenses/LICENSE.md,sha256=txSgV8n5zY1W3NiF5HHsCwlaW0e8We1cSC6TuJUqxXA,1060
95
+ mojentic-0.5.0.dist-info/METADATA,sha256=BRcX788NWyJ0K357r-xJ6s9slP8aqEXT-83FpN5LQzk,4935
96
+ mojentic-0.5.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
97
+ mojentic-0.5.0.dist-info/top_level.txt,sha256=Q-BvPQ8Eu1jnEqK8Xkr6A9C8Xa1z38oPZRHuA5MCTqg,19
98
+ mojentic-0.5.0.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