local-llm-kit 0.1.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.
examples/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """
2
+ Example scripts for local_llm_kit.
3
+ """
@@ -0,0 +1,66 @@
1
+ """
2
+ Simple example script for local_llm_kit chat.
3
+ """
4
+ import argparse
5
+ import sys
6
+
7
+ from local_llm_kit import chat
8
+
9
+ if __name__ == "__main__":
10
+ parser = argparse.ArgumentParser(description="Simple chat with a local LLM")
11
+ parser.add_argument("--model", "-m", required=True, help="Path to the model or model name")
12
+ parser.add_argument("--backend", "-b", choices=["transformers", "llamacpp"],
13
+ help="Backend to use (default: auto-detect)")
14
+ args = parser.parse_args()
15
+
16
+ # Initial message
17
+ print(f"Simple chat with {args.model}")
18
+ print("Type 'exit' or 'quit' to end the conversation.")
19
+ print()
20
+
21
+ # Chat loop
22
+ messages = [
23
+ {"role": "system", "content": "You are a helpful assistant."}
24
+ ]
25
+
26
+ try:
27
+ while True:
28
+ # Get user input
29
+ user_input = input("You: ")
30
+ if user_input.lower() in ["exit", "quit"]:
31
+ print("Goodbye!")
32
+ break
33
+
34
+ # Add user message
35
+ messages.append({"role": "user", "content": user_input})
36
+
37
+ # Generate and display response
38
+ try:
39
+ print("Assistant: ", end="", flush=True)
40
+ response_text = ""
41
+
42
+ # Stream the response token by token
43
+ for chunk in chat(
44
+ messages=messages,
45
+ model_path=args.model,
46
+ backend=args.backend,
47
+ stream=True
48
+ ):
49
+ # Extract and print chunk text
50
+ if "choices" in chunk and chunk["choices"] and "delta" in chunk["choices"][0]:
51
+ delta = chunk["choices"][0]["delta"]
52
+ if "content" in delta and delta["content"]:
53
+ print(delta["content"], end="", flush=True)
54
+ response_text += delta["content"]
55
+
56
+ print() # Add newline after response
57
+
58
+ # Add assistant response to history
59
+ messages.append({"role": "assistant", "content": response_text})
60
+
61
+ except Exception as e:
62
+ print(f"\nError: {e}")
63
+
64
+ except KeyboardInterrupt:
65
+ print("\nGoodbye!")
66
+ sys.exit(0)
@@ -0,0 +1,11 @@
1
+ """
2
+ local_llm_kit - OpenAI-like interface for local LLMs
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from .llm import LLM
8
+ from .chat import chat, complete
9
+ from .function_calling import add_function
10
+
11
+ __all__ = ["LLM", "chat", "complete", "add_function"]
@@ -0,0 +1,9 @@
1
+ """
2
+ Model backends for inference.
3
+ """
4
+
5
+ from .base import BaseBackend
6
+ from .transformers import TransformersBackend
7
+ from .llamacpp import LlamaCppBackend
8
+
9
+ __all__ = ["BaseBackend", "TransformersBackend", "LlamaCppBackend"]
@@ -0,0 +1,113 @@
1
+ """
2
+ Base class for model backends.
3
+ """
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Any, Iterator, Optional
7
+
8
+
9
+ class BaseBackend(ABC):
10
+ """
11
+ Abstract base class for model backends.
12
+
13
+ All backend implementations must inherit from this class and
14
+ implement the required methods.
15
+ """
16
+
17
+ @abstractmethod
18
+ def generate(
19
+ self,
20
+ prompt: str,
21
+ temperature: float = 0.7,
22
+ max_new_tokens: int = 512,
23
+ top_p: float = 0.95,
24
+ top_k: int = 40,
25
+ repetition_penalty: float = 1.1,
26
+ stream: bool = False,
27
+ logprobs: bool = False,
28
+ top_logprobs: Optional[int] = None,
29
+ **kwargs
30
+ ) -> Dict[str, Any]:
31
+ """
32
+ Generate text completion for a prompt.
33
+
34
+ Args:
35
+ prompt: Input text to complete
36
+ temperature: Sampling temperature (higher = more random)
37
+ max_new_tokens: Maximum number of tokens to generate
38
+ top_p: Top-p sampling parameter
39
+ top_k: Top-k sampling parameter
40
+ repetition_penalty: Penalty for repetition
41
+ stream: Whether to stream the response (should be False for this method)
42
+ logprobs: Whether to return log probabilities
43
+ top_logprobs: Number of top tokens to return logprobs for
44
+ **kwargs: Additional backend-specific parameters
45
+
46
+ Returns:
47
+ Dictionary with at least a "text" key containing the generated text
48
+ """
49
+ pass
50
+
51
+ @abstractmethod
52
+ def generate_stream(
53
+ self,
54
+ prompt: str,
55
+ temperature: float = 0.7,
56
+ max_new_tokens: int = 512,
57
+ top_p: float = 0.95,
58
+ top_k: int = 40,
59
+ repetition_penalty: float = 1.1,
60
+ logprobs: bool = False,
61
+ top_logprobs: Optional[int] = None,
62
+ **kwargs
63
+ ) -> Iterator[Dict[str, Any]]:
64
+ """
65
+ Stream text completion for a prompt.
66
+
67
+ Args:
68
+ prompt: Input text to complete
69
+ temperature: Sampling temperature (higher = more random)
70
+ max_new_tokens: Maximum number of tokens to generate
71
+ top_p: Top-p sampling parameter
72
+ top_k: Top-k sampling parameter
73
+ repetition_penalty: Penalty for repetition
74
+ logprobs: Whether to return log probabilities
75
+ top_logprobs: Number of top tokens to return logprobs for
76
+ **kwargs: Additional backend-specific parameters
77
+
78
+ Yields:
79
+ Dictionaries with at least a "text" key containing a chunk of generated text
80
+ """
81
+ pass
82
+
83
+ @abstractmethod
84
+ def get_context_window(self) -> int:
85
+ """
86
+ Get the context window size of the model.
87
+
88
+ Returns:
89
+ Maximum context window size in tokens
90
+ """
91
+ pass
92
+
93
+ @abstractmethod
94
+ def count_tokens(self, text: str) -> int:
95
+ """
96
+ Count the number of tokens in a text.
97
+
98
+ Args:
99
+ text: Input text
100
+
101
+ Returns:
102
+ Token count
103
+ """
104
+ pass
105
+
106
+ def get_timestamp(self) -> int:
107
+ """
108
+ Get current timestamp.
109
+
110
+ Returns:
111
+ Current timestamp in seconds
112
+ """
113
+ return int(time.time())
@@ -0,0 +1,293 @@
1
+ """
2
+ Backend for llama.cpp models via llama-cpp-python.
3
+ """
4
+ import logging
5
+ import time
6
+ import json
7
+ from typing import Dict, Any, Iterator, Optional, List, Callable, Union, Tuple
8
+
9
+ from .base import BaseBackend
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ try:
14
+ from llama_cpp import Llama, LlamaGrammar
15
+ LLAMA_CPP_AVAILABLE = True
16
+ except ImportError:
17
+ logger.warning("llama-cpp-python package not available. Install with 'pip install llama-cpp-python'")
18
+ LLAMA_CPP_AVAILABLE = False
19
+
20
+
21
+ class LlamaCppBackend(BaseBackend):
22
+ """
23
+ Backend for llama.cpp models via llama-cpp-python binding.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ model_path: str,
29
+ n_gpu_layers: int = -1,
30
+ n_ctx: int = 2048,
31
+ n_batch: int = 512,
32
+ verbose: bool = False,
33
+ **kwargs
34
+ ):
35
+ """
36
+ Initialize the llama.cpp backend.
37
+
38
+ Args:
39
+ model_path: Path to the model file (.gguf)
40
+ n_gpu_layers: Number of layers to offload to GPU (-1 = all)
41
+ n_ctx: Context window size
42
+ n_batch: Batch size for prompt evaluation
43
+ verbose: Whether to enable verbose output
44
+ **kwargs: Additional arguments to pass to Llama constructor
45
+ """
46
+ if not LLAMA_CPP_AVAILABLE:
47
+ raise ImportError(
48
+ "llama-cpp-python package is not installed. "
49
+ "Install it with 'pip install llama-cpp-python'"
50
+ )
51
+
52
+ self.model_path = model_path
53
+ self.n_ctx = n_ctx
54
+
55
+ # Load model
56
+ logger.info(f"Loading model from {model_path}")
57
+ self.model = Llama(
58
+ model_path=model_path,
59
+ n_gpu_layers=n_gpu_layers,
60
+ n_ctx=n_ctx,
61
+ n_batch=n_batch,
62
+ verbose=verbose,
63
+ **kwargs
64
+ )
65
+
66
+ def generate(
67
+ self,
68
+ prompt: str,
69
+ temperature: float = 0.7,
70
+ max_new_tokens: int = 512,
71
+ top_p: float = 0.95,
72
+ top_k: int = 40,
73
+ repetition_penalty: float = 1.1,
74
+ stream: bool = False,
75
+ logprobs: bool = False,
76
+ top_logprobs: Optional[int] = None,
77
+ **kwargs
78
+ ) -> Dict[str, Any]:
79
+ """Generate text completion for a prompt."""
80
+ if stream:
81
+ # If stream is requested, use generate_stream and collect all chunks
82
+ chunks = list(self.generate_stream(
83
+ prompt=prompt,
84
+ temperature=temperature,
85
+ max_new_tokens=max_new_tokens,
86
+ top_p=top_p,
87
+ top_k=top_k,
88
+ repetition_penalty=repetition_penalty,
89
+ logprobs=logprobs,
90
+ top_logprobs=top_logprobs,
91
+ **kwargs
92
+ ))
93
+
94
+ # Combine chunks
95
+ text = "".join(chunk["text"] for chunk in chunks)
96
+
97
+ # Combine logprobs if requested
98
+ combined_logprobs = None
99
+ if logprobs and chunks and "logprobs" in chunks[0]:
100
+ combined_logprobs = {
101
+ "tokens": [],
102
+ "token_logprobs": [],
103
+ "top_logprobs": []
104
+ }
105
+
106
+ for chunk in chunks:
107
+ if "logprobs" in chunk:
108
+ chunk_logprobs = chunk["logprobs"]
109
+ combined_logprobs["tokens"].extend(chunk_logprobs.get("tokens", []))
110
+ combined_logprobs["token_logprobs"].extend(chunk_logprobs.get("token_logprobs", []))
111
+
112
+ if "top_logprobs" in chunk_logprobs:
113
+ combined_logprobs["top_logprobs"].extend(chunk_logprobs.get("top_logprobs", []))
114
+
115
+ result = {"text": text}
116
+ if combined_logprobs:
117
+ result["logprobs"] = combined_logprobs
118
+
119
+ return result
120
+
121
+ # Setup generation parameters
122
+ params = {
123
+ "temperature": temperature,
124
+ "max_tokens": max_new_tokens,
125
+ "top_p": top_p,
126
+ "top_k": top_k,
127
+ "repeat_penalty": repetition_penalty,
128
+ "logprobs": top_logprobs if logprobs else None,
129
+ }
130
+
131
+ # Add grammar if provided
132
+ if "grammar" in kwargs:
133
+ grammar = kwargs.pop("grammar")
134
+ if isinstance(grammar, str):
135
+ params["grammar"] = LlamaGrammar.from_string(grammar)
136
+ elif isinstance(grammar, dict):
137
+ params["grammar"] = LlamaGrammar.from_string(json.dumps(grammar))
138
+
139
+ # Generate
140
+ output = self.model(
141
+ prompt=prompt,
142
+ **params,
143
+ **kwargs
144
+ )
145
+
146
+ # Extract text and tokens
147
+ if isinstance(output, dict):
148
+ text = output.get("choices", [{}])[0].get("text", "")
149
+ else:
150
+ # Handle list output format
151
+ text = output[0]["choices"][0]["text"]
152
+
153
+ result = {"text": text}
154
+
155
+ # Add logprobs if requested
156
+ if logprobs and top_logprobs:
157
+ try:
158
+ tokens_with_logprobs = []
159
+
160
+ if isinstance(output, dict) and "choices" in output:
161
+ choice = output["choices"][0]
162
+ if "logprobs" in choice:
163
+ tokens_with_logprobs = choice["logprobs"]["tokens"]
164
+ token_logprobs = choice["logprobs"]["token_logprobs"]
165
+ top_logprobs_data = choice["logprobs"]["top_logprobs"]
166
+ elif isinstance(output, list):
167
+ choice = output[0]["choices"][0]
168
+ if "logprobs" in choice:
169
+ tokens_with_logprobs = choice["logprobs"]["tokens"]
170
+ token_logprobs = choice["logprobs"]["token_logprobs"]
171
+ top_logprobs_data = choice["logprobs"]["top_logprobs"]
172
+
173
+ if tokens_with_logprobs:
174
+ result["logprobs"] = {
175
+ "tokens": tokens_with_logprobs,
176
+ "token_logprobs": token_logprobs,
177
+ "top_logprobs": top_logprobs_data
178
+ }
179
+ except (KeyError, IndexError, TypeError) as e:
180
+ logger.warning(f"Failed to extract logprobs: {e}")
181
+
182
+ return result
183
+
184
+ def generate_stream(
185
+ self,
186
+ prompt: str,
187
+ temperature: float = 0.7,
188
+ max_new_tokens: int = 512,
189
+ top_p: float = 0.95,
190
+ top_k: int = 40,
191
+ repetition_penalty: float = 1.1,
192
+ logprobs: bool = False,
193
+ top_logprobs: Optional[int] = None,
194
+ **kwargs
195
+ ) -> Iterator[Dict[str, Any]]:
196
+ """Stream text completion for a prompt."""
197
+ # Setup generation parameters
198
+ params = {
199
+ "temperature": temperature,
200
+ "max_tokens": max_new_tokens,
201
+ "top_p": top_p,
202
+ "top_k": top_k,
203
+ "repeat_penalty": repetition_penalty,
204
+ "logprobs": top_logprobs if logprobs else None,
205
+ "stream": True,
206
+ }
207
+
208
+ # Add grammar if provided
209
+ if "grammar" in kwargs:
210
+ grammar = kwargs.pop("grammar")
211
+ if isinstance(grammar, str):
212
+ params["grammar"] = LlamaGrammar.from_string(grammar)
213
+ elif isinstance(grammar, dict):
214
+ params["grammar"] = LlamaGrammar.from_string(json.dumps(grammar))
215
+
216
+ # Generate with streaming
217
+ for chunk in self.model(
218
+ prompt=prompt,
219
+ **params,
220
+ **kwargs
221
+ ):
222
+ # Extract delta text from chunk
223
+ if "choices" in chunk and chunk["choices"]:
224
+ delta_text = chunk["choices"][0].get("text", "")
225
+
226
+ # Build chunk result
227
+ result = {"text": delta_text}
228
+
229
+ # Add logprobs if available
230
+ if logprobs and top_logprobs:
231
+ try:
232
+ if "logprobs" in chunk["choices"][0]:
233
+ logprobs_data = chunk["choices"][0]["logprobs"]
234
+ result["logprobs"] = {
235
+ "tokens": logprobs_data.get("tokens", []),
236
+ "token_logprobs": logprobs_data.get("token_logprobs", []),
237
+ "top_logprobs": logprobs_data.get("top_logprobs", [])
238
+ }
239
+ except (KeyError, IndexError, TypeError) as e:
240
+ pass
241
+
242
+ yield result
243
+
244
+ def get_context_window(self) -> int:
245
+ """Get the context window size of the model."""
246
+ return self.n_ctx
247
+
248
+ def count_tokens(self, text: str) -> int:
249
+ """Count the number of tokens in a text."""
250
+ return len(self.model.tokenize(text.encode("utf-8")))
251
+
252
+
253
+ # Function to create JSON grammar for structured outputs
254
+ def create_json_grammar() -> str:
255
+ """
256
+ Create a grammar for JSON generation.
257
+
258
+ Returns:
259
+ Grammar string in BNF format
260
+ """
261
+ grammar = """
262
+ root ::= object
263
+
264
+ value ::= object | array | string | number | ("true" | "false" | "null")
265
+
266
+ object ::= "{" ws (string_colon_value (comma_string_colon_value)*)? ws "}"
267
+
268
+ string_colon_value ::= ws string ws ":" ws value
269
+
270
+ comma_string_colon_value ::= ws "," ws string ws ":" ws value
271
+
272
+ array ::= "[" ws (value (comma_value)*)? ws "]"
273
+
274
+ comma_value ::= ws "," ws value
275
+
276
+ string ::= "\"" (
277
+ [^"\\] |
278
+ "\\\\" |
279
+ "\\\"" |
280
+ "\\/" |
281
+ "\\b" |
282
+ "\\f" |
283
+ "\\n" |
284
+ "\\r" |
285
+ "\\t" |
286
+ "\\u" [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F] [0-9a-fA-F]
287
+ )* "\""
288
+
289
+ number ::= "-"? ("0" | [1-9] [0-9]*) ("." [0-9]+)? ([eE] [+-]? [0-9]+)?
290
+
291
+ ws ::= [ \\t\\n\\r]*
292
+ """
293
+ return grammar