h-ai-brain 0.0.23__py3-none-any.whl → 0.0.24__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.
Files changed (30) hide show
  1. h_ai/__init__.py +11 -2
  2. h_ai/application/__init__.py +12 -0
  3. h_ai/application/hai_service.py +12 -18
  4. h_ai/application/services/__init__.py +11 -0
  5. h_ai/application/services/base_model_service.py +69 -0
  6. h_ai/application/services/granite_service.py +139 -0
  7. h_ai/application/services/nomic_service.py +117 -0
  8. h_ai/domain/llm_config.py +14 -1
  9. h_ai/domain/model_factory.py +44 -0
  10. h_ai/domain/reasoning/llm_chat_repository.py +39 -2
  11. h_ai/domain/reasoning/llm_embedding_repository.py +20 -0
  12. h_ai/domain/reasoning/llm_generate_respository.py +21 -4
  13. h_ai/domain/reasoning/llm_tool_repository.py +24 -1
  14. h_ai/infrastructure/llm/json_resource_loader.py +97 -0
  15. h_ai/infrastructure/llm/ollama/factories/__init__.py +1 -0
  16. h_ai/infrastructure/llm/ollama/factories/granite_factory.py +91 -0
  17. h_ai/infrastructure/llm/ollama/factories/nomic_factory.py +58 -0
  18. h_ai/infrastructure/llm/ollama/ollama_chat_repository.py +165 -26
  19. h_ai/infrastructure/llm/ollama/ollama_embed_repository.py +43 -0
  20. h_ai/infrastructure/llm/ollama/ollama_generate_repository.py +88 -32
  21. h_ai/infrastructure/llm/ollama/ollama_http_client.py +54 -0
  22. h_ai/infrastructure/llm/prompt_loader.py +42 -7
  23. h_ai/infrastructure/llm/template_loader.py +146 -0
  24. {h_ai_brain-0.0.23.dist-info → h_ai_brain-0.0.24.dist-info}/METADATA +2 -1
  25. h_ai_brain-0.0.24.dist-info/RECORD +43 -0
  26. h_ai_brain-0.0.23.dist-info/RECORD +0 -30
  27. {h_ai_brain-0.0.23.dist-info → h_ai_brain-0.0.24.dist-info}/WHEEL +0 -0
  28. {h_ai_brain-0.0.23.dist-info → h_ai_brain-0.0.24.dist-info}/licenses/LICENSE +0 -0
  29. {h_ai_brain-0.0.23.dist-info → h_ai_brain-0.0.24.dist-info}/licenses/NOTICE.txt +0 -0
  30. {h_ai_brain-0.0.23.dist-info → h_ai_brain-0.0.24.dist-info}/top_level.txt +0 -0
h_ai/__init__.py CHANGED
@@ -1,3 +1,12 @@
1
- __all__ = ['HaiService']
1
+ """
2
+ h_ai package for interacting with LLM models.
2
3
 
3
- from .application.hai_service import HaiService
4
+ This package provides a high-level API for interacting with LLM models.
5
+ Users should import from this package, not from the application, domain, or infrastructure layers directly.
6
+ """
7
+
8
+ __all__ = ['GraniteService', 'NomicService', 'BaseModelService']
9
+
10
+ from .application.services.granite_service import GraniteService
11
+ from .application.services.nomic_service import NomicService
12
+ from .application.services.base_model_service import BaseModelService
@@ -0,0 +1,12 @@
1
+ """
2
+ Application layer for the h_ai package.
3
+
4
+ This package contains the public API for the h_ai package.
5
+ Users should only import from this package, not from the domain or infrastructure layers.
6
+ """
7
+
8
+ __all__ = ['GraniteService', 'NomicService', 'BaseModelService']
9
+
10
+ from .services.granite_service import GraniteService
11
+ from .services.nomic_service import NomicService
12
+ from .services.base_model_service import BaseModelService
@@ -1,18 +1,12 @@
1
- from ..domain.llm_config import LLMConfig
2
- from ..infrastructure.llm.ollama.ollama_generate_repository import OllamaGenerateRepository
3
-
4
-
5
- class HaiService:
6
- def __init__(self, llm_config: LLMConfig):
7
- self.llm_config = llm_config
8
- self.llm_generate_repository = OllamaGenerateRepository(
9
- self.llm_config.url,
10
- self.llm_config.model_name,
11
- temperature=self.llm_config.temperature,
12
- max_tokens=self.llm_config.max_tokens,
13
- api_token=self.llm_config.api_token)
14
-
15
- def ask_question(self, question: str, system_prompt: str = None, max_tokens = None) -> str:
16
- return self.llm_generate_repository.generate(question, system_prompt, max_tokens)
17
-
18
-
1
+ """
2
+ This file has been intentionally emptied as part of the removal of HaiService.
3
+ HaiService has been deprecated in favor of using GraniteService and NomicService directly.
4
+
5
+ The correct model names are:
6
+ - nomic-embed-text:137m-v1.5-fp16
7
+ - granite3.3:8b
8
+
9
+ Please use the model-specific services directly:
10
+ - GraniteService for text generation and chat
11
+ - NomicService for embeddings
12
+ """
@@ -0,0 +1,11 @@
1
+ """
2
+ Model-specific services for the h_ai package.
3
+
4
+ This package contains service classes for specific LLM models.
5
+ """
6
+
7
+ __all__ = ['GraniteService', 'NomicService', 'BaseModelService']
8
+
9
+ from .granite_service import GraniteService
10
+ from .nomic_service import NomicService
11
+ from .base_model_service import BaseModelService
@@ -0,0 +1,69 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, List, Optional
3
+
4
+
5
+ class BaseModelService(ABC):
6
+ """
7
+ Base class for model-specific services.
8
+ Defines the common interface that all model services must implement.
9
+ """
10
+
11
+ @abstractmethod
12
+ def generate(self, prompt: str, system_prompt: str = None, max_tokens: int = None) -> Optional[str]:
13
+ """
14
+ Generate text using the model.
15
+
16
+ Args:
17
+ prompt: The prompt to generate text from.
18
+ system_prompt: Optional system prompt to use.
19
+ max_tokens: Optional maximum number of tokens to generate.
20
+
21
+ Returns:
22
+ Optional[str]: The generated text, or None if an error occurs.
23
+ """
24
+ pass
25
+
26
+ @abstractmethod
27
+ def chat(self, message: str, session_id: str, chat_history: List[Dict[str, str]] = None) -> Optional[str]:
28
+ """
29
+ Chat with the model.
30
+
31
+ Args:
32
+ message: The user's message.
33
+ session_id: The ID of the chat session.
34
+ chat_history: Optional chat history to include in the conversation.
35
+
36
+ Returns:
37
+ Optional[str]: The model's response, or None if an error occurs.
38
+ """
39
+ pass
40
+
41
+ def embed(self, text: str) -> Optional[List[float]]:
42
+ """
43
+ Create an embedding for the given text.
44
+ Default implementation returns None as not all models support embeddings.
45
+
46
+ Args:
47
+ text: The text to create an embedding for.
48
+
49
+ Returns:
50
+ Optional[List[float]]: The embedding vector, or None if the model doesn't support embeddings.
51
+ """
52
+ return None
53
+
54
+ def chat_with_documents(self, message: str, session_id: str, documents: List[Dict[str, str]],
55
+ chat_history: List[Dict[str, str]] = None) -> Optional[str]:
56
+ """
57
+ Chat with the model using provided documents as context.
58
+ Default implementation returns None as not all models support document-based chat.
59
+
60
+ Args:
61
+ message: The user's message.
62
+ session_id: The ID of the chat session.
63
+ documents: List of documents to use as context.
64
+ chat_history: Optional chat history to include in the conversation.
65
+
66
+ Returns:
67
+ Optional[str]: The model's response, or None if the model doesn't support document-based chat.
68
+ """
69
+ return None
@@ -0,0 +1,139 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from .base_model_service import BaseModelService
4
+ from ...infrastructure.llm.ollama.factories.granite_factory import GraniteModelFactory
5
+ from ...infrastructure.llm.ollama.models.ollama_chat_message import OllamaChatMessage
6
+
7
+
8
+ class GraniteService(BaseModelService):
9
+ """
10
+ Service for interacting with the Granite model.
11
+ Provides specialized methods for the Granite model, including document support.
12
+ """
13
+
14
+ def __init__(self, api_url: str, api_token: str = None, temperature: float = 0.6, max_tokens: int = 2500):
15
+ """
16
+ Initialize a new GraniteService.
17
+
18
+ Args:
19
+ api_url: The base URL of the LLM API.
20
+ api_token: Optional API token for authentication.
21
+ temperature: The temperature to use for generation.
22
+ max_tokens: The maximum number of tokens to generate.
23
+
24
+ Raises:
25
+ ValueError: If the api_url is empty or None, or if temperature or max_tokens are invalid.
26
+ """
27
+ if not api_url:
28
+ raise ValueError("API URL cannot be empty or None")
29
+ if temperature < 0:
30
+ raise ValueError("Temperature must be non-negative")
31
+ if max_tokens <= 0:
32
+ raise ValueError("Max tokens must be positive")
33
+
34
+ self.api_url = api_url
35
+ self.api_token = api_token
36
+ self.temperature = temperature
37
+ self.max_tokens = max_tokens
38
+
39
+ # Create the Granite model factory
40
+ self.factory = GraniteModelFactory(
41
+ api_url=api_url,
42
+ temperature=temperature,
43
+ max_tokens=max_tokens,
44
+ api_token=api_token,
45
+ use_templating=True
46
+ )
47
+
48
+ # Create repositories
49
+ self.chat_repository = self.factory.create_chat_repository()
50
+ self.generate_repository = self.factory.create_generate_repository()
51
+
52
+ def generate(self, prompt: str, system_prompt: str = None, max_tokens: int = None) -> Optional[str]:
53
+ """
54
+ Generate text using the Granite model.
55
+
56
+ Args:
57
+ prompt: The prompt to generate text from.
58
+ system_prompt: Optional system prompt to use.
59
+ max_tokens: Optional maximum number of tokens to generate.
60
+
61
+ Returns:
62
+ Optional[str]: The generated text, or None if an error occurs.
63
+
64
+ Raises:
65
+ ValueError: If the prompt is empty or None.
66
+ """
67
+ if not prompt:
68
+ raise ValueError("Prompt cannot be empty or None")
69
+
70
+ return self.generate_repository.generate(prompt, system_prompt, max_tokens)
71
+
72
+ def chat(self, message: str, session_id: str, chat_history: List[Dict[str, str]] = None) -> Optional[str]:
73
+ """
74
+ Chat with the Granite model.
75
+
76
+ Args:
77
+ message: The user's message.
78
+ session_id: The ID of the chat session.
79
+ chat_history: Optional chat history to include in the conversation.
80
+
81
+ Returns:
82
+ Optional[str]: The model's response, or None if an error occurs.
83
+
84
+ Raises:
85
+ ValueError: If the message is empty or None, or if the session_id is empty or None.
86
+ """
87
+ if not message:
88
+ raise ValueError("Message cannot be empty or None")
89
+ if not session_id:
90
+ raise ValueError("Session ID cannot be empty or None")
91
+
92
+ return self.chat_repository.chat(message, session_id, chat_history)
93
+
94
+ def chat_with_documents(self, message: str, session_id: str, documents: List[Dict[str, str]],
95
+ chat_history: List[Dict[str, str]] = None) -> Optional[str]:
96
+ """
97
+ Chat with the Granite model using provided documents as context.
98
+
99
+ Args:
100
+ message: The user's message.
101
+ session_id: The ID of the chat session.
102
+ documents: List of documents to use as context. Each document should have 'id' and 'content' keys.
103
+ chat_history: Optional chat history to include in the conversation.
104
+
105
+ Returns:
106
+ Optional[str]: The model's response, or None if an error occurs.
107
+
108
+ Raises:
109
+ ValueError: If the message is empty or None, or if the session_id is empty or None.
110
+ """
111
+ if not message:
112
+ raise ValueError("Message cannot be empty or None")
113
+ if not session_id:
114
+ raise ValueError("Session ID cannot be empty or None")
115
+ if not documents:
116
+ raise ValueError("Documents list cannot be empty or None")
117
+
118
+ # Start with system prompts and user message
119
+ messages = []
120
+
121
+ # Add document messages
122
+ for doc in documents:
123
+ doc_id = doc.get('id', '')
124
+ content = doc.get('content', '')
125
+ if content:
126
+ # Use the document role with optional ID
127
+ role = f"document{doc_id}" if doc_id else "document"
128
+ messages.append(OllamaChatMessage(role, content))
129
+
130
+ # Add chat history if provided
131
+ if chat_history:
132
+ for msg in chat_history:
133
+ messages.append(OllamaChatMessage(msg.get("role", "user"), msg.get("content", "")))
134
+
135
+ # Add the current user message
136
+ messages.append(OllamaChatMessage("user", message))
137
+
138
+ # Call the chat repository with the prepared messages
139
+ return self.chat_repository.chat_with_messages(messages)
@@ -0,0 +1,117 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from .base_model_service import BaseModelService
4
+ from ...infrastructure.llm.ollama.factories.nomic_factory import NomicModelFactory
5
+
6
+
7
+ class NomicService(BaseModelService):
8
+ """
9
+ Service for interacting with the Nomic model.
10
+ Provides specialized methods for the Nomic model, focusing on embeddings.
11
+ """
12
+
13
+ def __init__(self, api_url: str, api_token: str = None):
14
+ """
15
+ Initialize a new NomicService.
16
+
17
+ Args:
18
+ api_url: The base URL of the LLM API.
19
+ api_token: Optional API token for authentication.
20
+
21
+ Raises:
22
+ ValueError: If the api_url is empty or None.
23
+ """
24
+ if not api_url:
25
+ raise ValueError("API URL cannot be empty or None")
26
+
27
+ self.api_url = api_url
28
+ self.api_token = api_token
29
+
30
+ # Create the Nomic model factory
31
+ self.factory = NomicModelFactory(
32
+ api_url=api_url,
33
+ api_token=api_token
34
+ )
35
+
36
+ # Create repositories
37
+ self.embedding_repository = self.factory.create_embedding_repository()
38
+
39
+ def generate(self, prompt: str, system_prompt: str = None, max_tokens: int = None) -> Optional[str]:
40
+ """
41
+ Generate text using the Nomic model.
42
+ Nomic doesn't support text generation, so this always returns None.
43
+
44
+ Args:
45
+ prompt: The prompt to generate text from.
46
+ system_prompt: Optional system prompt to use.
47
+ max_tokens: Optional maximum number of tokens to generate.
48
+
49
+ Returns:
50
+ Optional[str]: Always None as Nomic doesn't support text generation.
51
+ """
52
+ return None
53
+
54
+ def chat(self, message: str, session_id: str, chat_history: List[Dict[str, str]] = None) -> Optional[str]:
55
+ """
56
+ Chat with the Nomic model.
57
+ Nomic doesn't support chat, so this always returns None.
58
+
59
+ Args:
60
+ message: The user's message.
61
+ session_id: The ID of the chat session.
62
+ chat_history: Optional chat history to include in the conversation.
63
+
64
+ Returns:
65
+ Optional[str]: Always None as Nomic doesn't support chat.
66
+ """
67
+ return None
68
+
69
+ def embed(self, text: str) -> Optional[List[float]]:
70
+ """
71
+ Create an embedding for the given text using the Nomic model.
72
+
73
+ Args:
74
+ text: The text to create an embedding for.
75
+
76
+ Returns:
77
+ Optional[List[float]]: The embedding vector, or None if an error occurs.
78
+
79
+ Raises:
80
+ ValueError: If the text is empty or None.
81
+ """
82
+ if not text:
83
+ raise ValueError("Text cannot be empty or None")
84
+
85
+ return self.embedding_repository.embed(text)
86
+
87
+ def batch_embed(self, texts: List[str]) -> Optional[List[List[float]]]:
88
+ """
89
+ Create embeddings for multiple texts in a single batch.
90
+
91
+ Args:
92
+ texts: List of texts to create embeddings for.
93
+
94
+ Returns:
95
+ Optional[List[List[float]]]: List of embedding vectors, or None if an error occurs.
96
+
97
+ Raises:
98
+ ValueError: If the texts list is empty or None.
99
+ """
100
+ if not texts:
101
+ raise ValueError("Texts list cannot be empty or None")
102
+
103
+ # Filter out empty texts
104
+ valid_texts = [text for text in texts if text]
105
+ if not valid_texts:
106
+ raise ValueError("All texts in the list are empty")
107
+
108
+ # Process each text individually
109
+ # In a real implementation, this could be optimized to use batch processing if supported by the API
110
+ embeddings = []
111
+ for text in valid_texts:
112
+ embedding = self.embedding_repository.embed(text)
113
+ if embedding is None:
114
+ return None # If any embedding fails, return None
115
+ embeddings.append(embedding)
116
+
117
+ return embeddings
h_ai/domain/llm_config.py CHANGED
@@ -1,7 +1,20 @@
1
1
  class LLMConfig:
2
+ """
3
+ Configuration for an LLM service.
4
+ """
2
5
  def __init__(self, url: str, model_name: str, temperature: float = 0.6, max_tokens: int = 2500, api_token: str = None):
6
+ """
7
+ Initialize a new LLMConfig.
8
+
9
+ Args:
10
+ url: The base URL of the LLM service.
11
+ model_name: The name of the model to use.
12
+ temperature: The temperature to use for generation (higher = more creative, lower = more deterministic).
13
+ max_tokens: The maximum number of tokens to generate.
14
+ api_token: Optional API token for authentication.
15
+ """
3
16
  self.url = url
4
17
  self.model_name = model_name
5
18
  self.temperature = temperature
6
19
  self.max_tokens = max_tokens
7
- self.api_token = api_token
20
+ self.api_token = api_token
@@ -0,0 +1,44 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
3
+
4
+ from .reasoning.llm_chat_repository import LlmChatRepository
5
+ from .reasoning.llm_embedding_repository import LlmEmbeddingRepository
6
+ from .reasoning.llm_generate_respository import LlmGenerateRepository
7
+
8
+
9
+ class ModelFactory(ABC):
10
+ """
11
+ Abstract factory for creating model-specific repositories.
12
+ This factory is responsible for creating the appropriate repositories for a given model.
13
+ """
14
+
15
+ @abstractmethod
16
+ def create_chat_repository(self) -> LlmChatRepository:
17
+ """
18
+ Creates a chat repository for the model.
19
+
20
+ Returns:
21
+ LlmChatRepository: A repository for chat interactions with the model.
22
+ """
23
+ pass
24
+
25
+ @abstractmethod
26
+ def create_generate_repository(self) -> LlmGenerateRepository:
27
+ """
28
+ Creates a generate repository for the model.
29
+
30
+ Returns:
31
+ LlmGenerateRepository: A repository for text generation with the model.
32
+ """
33
+ pass
34
+
35
+ @abstractmethod
36
+ def create_embedding_repository(self) -> Optional[LlmEmbeddingRepository]:
37
+ """
38
+ Creates an embedding repository for the model.
39
+
40
+ Returns:
41
+ Optional[LlmEmbeddingRepository]: A repository for creating embeddings with the model,
42
+ or None if the model doesn't support embeddings.
43
+ """
44
+ pass
@@ -1,9 +1,46 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Optional
2
+ from typing import Optional, List, Dict, Any, Protocol
3
+
4
+
5
+ class ChatMessage(Protocol):
6
+ """Protocol for chat messages."""
7
+ role: str
8
+ content: str
3
9
 
4
10
 
5
11
  class LlmChatRepository(ABC):
12
+ """
13
+ Repository for chat interactions with an LLM.
14
+ """
15
+
16
+ @abstractmethod
17
+ def chat(self, user_message: str, session_id: str, chat_history: List[Dict[str, str]] = None) -> Optional[str]:
18
+ """
19
+ Chat with the LLM.
20
+
21
+ Args:
22
+ user_message: The user's message.
23
+ session_id: The ID of the chat session.
24
+ chat_history: Optional chat history to include in the conversation.
25
+
26
+ Returns:
27
+ Optional[str]: The LLM's response, or None if the request failed.
28
+ """
29
+ ...
6
30
 
7
31
  @abstractmethod
8
- def chat(self, user_message: str, session_id: str) -> Optional[str]:
32
+ def chat_with_messages(self, messages: List[Any]) -> Optional[str]:
33
+ """
34
+ Chat with the model using a custom list of messages.
35
+
36
+ This method allows for more flexibility in message formatting, such as including
37
+ document messages or other special message types.
38
+
39
+ Args:
40
+ messages: The list of messages to send to the model. Each message should have
41
+ 'role' and 'content' attributes or keys.
42
+
43
+ Returns:
44
+ Optional[str]: The model's response, or None if the request failed.
45
+ """
9
46
  ...
@@ -0,0 +1,20 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List, Optional
3
+
4
+ class LlmEmbeddingRepository(ABC):
5
+ """
6
+ Repository for creating embeddings with an LLM.
7
+ """
8
+
9
+ @abstractmethod
10
+ def embed(self, text: str) -> Optional[List[float]]:
11
+ """
12
+ Create an embedding for the given text.
13
+
14
+ Args:
15
+ text: The text to create an embedding for.
16
+
17
+ Returns:
18
+ Optional[List[float]]: The embedding vector, or None if the request failed.
19
+ """
20
+ pass
@@ -1,6 +1,23 @@
1
- from typing import Protocol
1
+ from abc import ABC, abstractmethod
2
+ from typing import Optional
2
3
 
3
4
 
4
- class LlmGenerateRepository(Protocol):
5
- def generate(self, user_prompt: str, system_prompt: str, session_id: str = None) -> str:
6
- ...
5
+ class LlmGenerateRepository(ABC):
6
+ """
7
+ Repository for text generation with an LLM.
8
+ """
9
+
10
+ @abstractmethod
11
+ def generate(self, user_prompt: str, system_prompt: str = None, max_tokens: int = None) -> Optional[str]:
12
+ """
13
+ Generate text using the LLM.
14
+
15
+ Args:
16
+ user_prompt: The prompt to generate text from.
17
+ system_prompt: Optional system prompt to use.
18
+ max_tokens: Optional maximum number of tokens to generate.
19
+
20
+ Returns:
21
+ Optional[str]: The generated text, or None if the request failed.
22
+ """
23
+ ...
@@ -5,10 +5,33 @@ from ...domain.reasoning.tool_message import ToolMessage
5
5
 
6
6
 
7
7
  class LlmToolRepository(ABC):
8
+ """
9
+ Repository for tool interactions with an LLM.
10
+ """
11
+
8
12
  @abstractmethod
9
13
  def find_tools_in_message(self, message: str) -> List[ToolMessage] | None:
14
+ """
15
+ Extract tool calls from a message.
16
+
17
+ Args:
18
+ message: The message to extract tool calls from.
19
+
20
+ Returns:
21
+ List[ToolMessage] | None: A list of tool messages, or None if no tools were found.
22
+ """
10
23
  ...
11
24
 
12
25
  @abstractmethod
13
- def build_tool_response_prompt(self, question: str, tool_results: list[str])-> str|None:
26
+ def build_tool_response_prompt(self, question: str, tool_results: list[str]) -> str | None:
27
+ """
28
+ Build a prompt for the LLM to respond to tool results.
29
+
30
+ Args:
31
+ question: The original question that triggered the tool calls.
32
+ tool_results: The results from executing the tools.
33
+
34
+ Returns:
35
+ str | None: The prompt for the LLM, or None if the prompt could not be built.
36
+ """
14
37
  ...