HowdenLLM 3.1.2__tar.gz → 4.0.0__tar.gz

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.
@@ -3,6 +3,7 @@ from typing import Callable
3
3
  from dotenv import load_dotenv
4
4
  from pathlib import Path
5
5
  from HowdenLLM.providers.provider_factory import ProviderFactory
6
+ from HowdenCommonObjects.howden_result import HowdenResult
6
7
  from typing import Any
7
8
  import hashlib
8
9
  import os
@@ -61,16 +62,7 @@ class LLM:
61
62
  self.total_output_tokens = 0
62
63
  self.total_runs = 0
63
64
 
64
- def _count_tokens(self, text: str) -> int:
65
- """Try to count tokens with tiktoken; fallback to rough word count."""
66
- try:
67
- import tiktoken
68
- enc = tiktoken.encoding_for_model(self.model)
69
- return len(enc.encode(text))
70
- except (KeyError, LookupError, AttributeError, ImportError):
71
- return len(text.split())
72
-
73
- async def __call__(self, path_or_content: Path | str) -> str:
65
+ async def __call__(self, path_or_content: Path | str) -> HowdenResult:
74
66
  """
75
67
  Execute one completion round and return:
76
68
  (output_text, input_token_count, output_token_count)
@@ -84,29 +76,10 @@ class LLM:
84
76
 
85
77
  prompt = self.template.substitute(content=content)
86
78
 
87
- # --- count input tokens ---
88
- input_text = f"{self.system or ''}\n{prompt}"
89
- input_tokens = self._count_tokens(input_text)
90
-
91
79
  # --- run model ---
92
- output = await self.provider.complete(self.system, prompt, self.model, self.use_web_search_tool,self.upload_attachment)
93
-
94
- # --- count output tokens ---
95
- output_tokens = self._count_tokens(output)
96
-
97
- # --- update totals ---
98
- self.total_input_tokens += input_tokens
99
- self.total_output_tokens += output_tokens
100
- self.total_runs += 1
101
- print(f"[{self.name or 'LLM'}] "
102
- f"Input tokens: {input_tokens}, "
103
- f"Output tokens: {output_tokens}, "
104
- f"Total_input: {self.total_input_tokens}, "
105
- f"Total_output: {self.total_output_tokens}, "
106
- f"Total_input_average: {round(self.total_input_tokens / self.total_runs, 2)}, "
107
- f"Total_output_average: {round(self.total_output_tokens / self.total_runs, 2)}")
80
+ result = await self.provider.complete(self.name, self.system, prompt, self.model, self.use_web_search_tool,self.upload_attachment)
108
81
 
109
- return output
82
+ return result
110
83
 
111
84
  def make_serializable(self, obj: Any) -> Any:
112
85
  if obj is None or isinstance(obj, (str, int, float, bool)):
@@ -158,7 +131,7 @@ class LLMSwitch:
158
131
  A class that inspects a file to determine which LLM instance to invoke based on file content.
159
132
  Changes made to this class by Casper 19/11-25
160
133
  """
161
- def __init__(self, LLMs: list[LLM], name: str, json_key: str = None) -> None:
134
+ def __init__(self, LLMs: list[LLM], name: str = None, json_key: str = None) -> None:
162
135
  """
163
136
  Parameters
164
137
  ----------
@@ -172,6 +145,9 @@ class LLMSwitch:
172
145
  self.input_parameter = {llm.name: llm.input_params for llm in LLMs}
173
146
  self.json_key = json_key
174
147
  self.name = name
148
+ self.system: str # Set during __call__ to the params of the called LLM
149
+ self.template: str # Set during __call__ to the params of the called LLM
150
+ self.model: str # Set during __call__ to the params of the called LLM
175
151
 
176
152
  def _get_json_content(self, filepath: Path) -> str:
177
153
  """
@@ -203,7 +179,7 @@ class LLMSwitch:
203
179
  else:
204
180
  raise NotJsonException(filepath)
205
181
 
206
- def __call__(self, content_path: Path, filepath: Path) -> str:
182
+ async def __call__(self, content_path: Path, filepath: Path) -> HowdenResult:
207
183
  """
208
184
  Reads a file to determine which of the provided LLM objects to call. The method checks each model in
209
185
  ` self.LLMS` and calls the first model whose `name` appears in the content.
@@ -234,11 +210,14 @@ class LLMSwitch:
234
210
  content = filepath.read_text(encoding="utf-8")
235
211
  else:
236
212
  raise UnsupportedFiletypeException(filepath)
237
- result = ""
213
+ result = HowdenResult("")
238
214
 
239
215
  for model in self.LLMS:
240
216
  if model.name in content:
241
- result = model(content_path)
217
+ self.system = model.system
218
+ self.template = model.template
219
+ self.model = model.model
220
+ result = await model(content_path)
242
221
  return result
243
222
 
244
223
  def make_serializable(self, obj: Any) -> Any:
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+ from .provider_meta import ProviderMeta
3
+ from HowdenCommonObjects.howden_result import HowdenResult
4
+
5
+ class BaseProvider(ABC, metaclass=ProviderMeta):
6
+ @abstractmethod
7
+ async def complete(self, name:str, system: str, prompt: str, model: str, use_web_search_tool: bool, upload_attachment: str|None) -> HowdenResult:
8
+ pass
@@ -1,6 +1,8 @@
1
1
  from abc import ABC
2
2
  from HowdenLLM.providers.base_provider import BaseProvider
3
3
  from anthropic import AsyncAnthropic
4
+ from HowdenCommonObjects.howden_result import HowdenResult
5
+ from HowdenCommonObjects.usage import Usage
4
6
  from .known_providers import KnownProviders
5
7
 
6
8
  class AnthropicProvider(BaseProvider, ABC):
@@ -9,7 +11,7 @@ class AnthropicProvider(BaseProvider, ABC):
9
11
  def __init__(self,client: AsyncAnthropic):
10
12
  self.client = client
11
13
 
12
- async def complete(self, system: str, prompt: str, model: str, use_web_search_tool: bool, upload_attachment: str) -> str:
14
+ async def complete(self, name:str, system: str, prompt: str, model: str, use_web_search_tool: bool, upload_attachment: str|None) -> HowdenResult:
13
15
 
14
16
  if use_web_search_tool:
15
17
  tools=[{"type": "web_search_20260209","name": "web_search","max_uses": 5}]
@@ -17,7 +19,7 @@ class AnthropicProvider(BaseProvider, ABC):
17
19
  tools = []
18
20
 
19
21
  if model in ["claude-opus-4-7", "claude-opus-4-8","claude-opus-4-6","claude-opus-4-5-20251101","claude-sonnet-4-6","claude-sonnet-4-5-20250929"]:
20
- message = await self.client.messages.create(
22
+ response = await self.client.messages.create(
21
23
  model=model,
22
24
  system=system,
23
25
  messages=[
@@ -33,10 +35,15 @@ class AnthropicProvider(BaseProvider, ABC):
33
35
  else:
34
36
  raise Exception(f"Unsupported model: {model}")
35
37
 
36
- # print(f"Content: {message}")
37
- clean_response = "\n".join(
38
- block.text for block in message.content if block.type == "text"
38
+ clean_response = "\n".join(block.text for block in response.content if block.type == "text")
39
+ usage = Usage(
40
+ type="llm",
41
+ provider=self.provider,
42
+ model=model,
43
+ operation=name,
44
+ input_tokens=response.usage.input_tokens,
45
+ cached_tokens=response.usage.cache_read_input_tokens,
46
+ output_tokens=response.usage.output_tokens,
39
47
  )
40
48
 
41
-
42
- return clean_response
49
+ return HowdenResult(clean_response, usage)
@@ -1,7 +1,8 @@
1
1
  from abc import ABC
2
2
 
3
3
  from sympy.codegen.ast import continue_
4
-
4
+ from HowdenCommonObjects.howden_result import HowdenResult
5
+ from HowdenCommonObjects.usage import Usage
5
6
  from HowdenLLM.providers.base_provider import BaseProvider
6
7
  from openai import AsyncOpenAI
7
8
  from .known_providers import KnownProviders
@@ -12,7 +13,7 @@ class OpenAIProvider(BaseProvider, ABC):
12
13
  def __init__(self,client: AsyncOpenAI):
13
14
  self.client = client
14
15
 
15
- async def complete(self, system: str, prompt: str, model: str, use_web_search_tool: bool, upload_attachment: str|None) -> str:
16
+ async def complete(self, name:str, system: str, prompt: str, model: str, use_web_search_tool: bool, upload_attachment: str|None) -> HowdenResult:
16
17
 
17
18
  if use_web_search_tool:
18
19
  tools = [
@@ -23,7 +24,7 @@ class OpenAIProvider(BaseProvider, ABC):
23
24
 
24
25
  if upload_attachment:
25
26
  with open(upload_attachment, "rb") as f:
26
- uploaded = self.client.files.create(
27
+ uploaded = await self.client.files.create(
27
28
  file=f,
28
29
  purpose="user_data",
29
30
  )
@@ -50,4 +51,14 @@ class OpenAIProvider(BaseProvider, ABC):
50
51
  else:
51
52
  raise Exception(f"Unsupported model: {model}")
52
53
 
53
- return response.output_text
54
+ usage = Usage(
55
+ type="llm",
56
+ provider=self.provider,
57
+ model=model,
58
+ operation=name,
59
+ input_tokens=response.usage.input_tokens,
60
+ cached_tokens=response.usage.input_tokens_details.cached_tokens,
61
+ output_tokens=response.usage.output_tokens,
62
+ )
63
+
64
+ return HowdenResult(response.output_text, usage)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: HowdenLLM
3
- Version: 3.1.2
3
+ Version: 4.0.0
4
4
  Summary: A simple configuration manager with Pydantic and JSON export.
5
5
  License: MIT
6
6
  Keywords: config,configuration,pydantic,json
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Dist: accelerate (>=1.10.0,<2.0.0)
15
15
  Requires-Dist: anthropic (>=0.85.0,<0.86.0)
16
+ Requires-Dist: howdencommonobjects (>=1.1.0)
16
17
  Requires-Dist: langchain (>=1.1.3,<2.0.0)
17
18
  Requires-Dist: langchain-community (>=0.3.27,<0.4.0)
18
19
  Requires-Dist: openai (>=1.99.9,<2.0.0)
@@ -1,10 +1,10 @@
1
1
  [project]
2
2
  name = "HowdenLLM"
3
- version = "3.1.2"
3
+ version = "4.0.0"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12,<3.14"
7
- dependencies = [ "transformers (>=4.55.0,<5.0.0)", "pydantic (>=2.11.7,<3.0.0)", "python-dotenv (>=1.1.1,<2.0.0)", "langchain-community (>=0.3.27,<0.4.0)", "openai (>=1.99.9,<2.0.0)", "accelerate (>=1.10.0,<2.0.0)", "tiktoken (>=0.12.0,<0.13.0)", "langchain (>=1.1.3,<2.0.0)", "pytest (>=9.0.2,<10.0.0)", "anthropic (>=0.85.0,<0.86.0)",]
7
+ dependencies = [ "transformers (>=4.55.0,<5.0.0)", "pydantic (>=2.11.7,<3.0.0)", "python-dotenv (>=1.1.1,<2.0.0)", "langchain-community (>=0.3.27,<0.4.0)", "openai (>=1.99.9,<2.0.0)", "accelerate (>=1.10.0,<2.0.0)", "tiktoken (>=0.12.0,<0.13.0)", "langchain (>=1.1.3,<2.0.0)", "pytest (>=9.0.2,<10.0.0)", "anthropic (>=0.85.0,<0.86.0)","howdencommonobjects>=1.1.0"]
8
8
  [[project.authors]]
9
9
  name = "JesperThoftIllemannJ"
10
10
  email = "jesper.jaeger@howdendanmark.dk"
@@ -1,7 +0,0 @@
1
- from abc import ABC, abstractmethod
2
- from .provider_meta import ProviderMeta
3
-
4
- class BaseProvider(ABC, metaclass=ProviderMeta):
5
- @abstractmethod
6
- async def complete(self, system: str, prompt: str, model: str, use_web_search_tool: bool, upload_attachment: str|None) -> str:
7
- pass
File without changes