pythonai-grounded-generator 0.1.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.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonai-grounded-generator
3
+ Version: 0.1.0
4
+ Summary: Lightweight grounded text generation from retrieved context
5
+ Keywords: llm,rag,grounded-generation,transformers,qwen
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: torch>=2.2
13
+ Requires-Dist: transformers>=4.51.0
14
+
15
+ # PythonAI Grounded Generator
16
+
17
+ A compact Python helper for generating answers from retrieved context with an already loaded Hugging Face causal language model.
18
+
19
+ It provides:
20
+
21
+ - Custom system prompts and questions
22
+ - Token-budget-aware context truncation
23
+ - Qwen3 non-thinking chat formatting
24
+ - CPU or CUDA inference (device controlled by the caller)
25
+ - A plain-text answer suitable for wrapping in application-specific Pydantic models
26
+
27
+ Retrieval, context assembly, and structured JSON serialization remain the responsibility of the calling application. This package does **not** guarantee factual correctness or replace source validation.
28
+
29
+ ## Installation
30
+
31
+ After publication:
32
+
33
+ ```bash
34
+ pip install pythonai-grounded-generator
35
+ ```
36
+
37
+ Or install the local project:
38
+
39
+ ```bash
40
+ pip install .
41
+ ```
42
+
43
+ ## Example
44
+
45
+ ```python
46
+ from transformers import AutoModelForCausalLM, AutoTokenizer
47
+ from pythonai_grounded import generate_answer
48
+
49
+ name = "Qwen/Qwen3-0.6B"
50
+ tokenizer = AutoTokenizer.from_pretrained(name)
51
+ model = AutoModelForCausalLM.from_pretrained(name).to("cpu")
52
+ model.eval()
53
+
54
+ answer = generate_answer(
55
+ model=model,
56
+ tokenizer=tokenizer,
57
+ question="Which endpoint loads an adapter?",
58
+ context="Use POST /v1/load_lora_adapter to load an adapter.",
59
+ system_prompt="Answer only from the context. Copy technical names exactly.",
60
+ max_new_tokens=64,
61
+ device="cpu",
62
+ )
63
+ print(answer)
64
+ ```
65
+
66
+ ## API
67
+
68
+ ```python
69
+ generate_answer(
70
+ model,
71
+ tokenizer,
72
+ question: str,
73
+ context: str,
74
+ system_prompt: str,
75
+ max_new_tokens: int = 64,
76
+ device: str = "cpu",
77
+ ) -> str
78
+ ```
79
+
80
+ The supplied `model` and `tokenizer` must be compatible. The model must already be on the requested device and in evaluation mode. Model context limits are read from `model.config.max_position_embeddings`, falling back to 32768 if absent. Check actual model capabilities when switching models.
81
+
82
+ ## Build
83
+
84
+ ```bash
85
+ uv build
86
+ ```
87
+
88
+ The distributions are created in `dist/`.
@@ -0,0 +1,74 @@
1
+ # PythonAI Grounded Generator
2
+
3
+ A compact Python helper for generating answers from retrieved context with an already loaded Hugging Face causal language model.
4
+
5
+ It provides:
6
+
7
+ - Custom system prompts and questions
8
+ - Token-budget-aware context truncation
9
+ - Qwen3 non-thinking chat formatting
10
+ - CPU or CUDA inference (device controlled by the caller)
11
+ - A plain-text answer suitable for wrapping in application-specific Pydantic models
12
+
13
+ Retrieval, context assembly, and structured JSON serialization remain the responsibility of the calling application. This package does **not** guarantee factual correctness or replace source validation.
14
+
15
+ ## Installation
16
+
17
+ After publication:
18
+
19
+ ```bash
20
+ pip install pythonai-grounded-generator
21
+ ```
22
+
23
+ Or install the local project:
24
+
25
+ ```bash
26
+ pip install .
27
+ ```
28
+
29
+ ## Example
30
+
31
+ ```python
32
+ from transformers import AutoModelForCausalLM, AutoTokenizer
33
+ from pythonai_grounded import generate_answer
34
+
35
+ name = "Qwen/Qwen3-0.6B"
36
+ tokenizer = AutoTokenizer.from_pretrained(name)
37
+ model = AutoModelForCausalLM.from_pretrained(name).to("cpu")
38
+ model.eval()
39
+
40
+ answer = generate_answer(
41
+ model=model,
42
+ tokenizer=tokenizer,
43
+ question="Which endpoint loads an adapter?",
44
+ context="Use POST /v1/load_lora_adapter to load an adapter.",
45
+ system_prompt="Answer only from the context. Copy technical names exactly.",
46
+ max_new_tokens=64,
47
+ device="cpu",
48
+ )
49
+ print(answer)
50
+ ```
51
+
52
+ ## API
53
+
54
+ ```python
55
+ generate_answer(
56
+ model,
57
+ tokenizer,
58
+ question: str,
59
+ context: str,
60
+ system_prompt: str,
61
+ max_new_tokens: int = 64,
62
+ device: str = "cpu",
63
+ ) -> str
64
+ ```
65
+
66
+ The supplied `model` and `tokenizer` must be compatible. The model must already be on the requested device and in evaluation mode. Model context limits are read from `model.config.max_position_embeddings`, falling back to 32768 if absent. Check actual model capabilities when switching models.
67
+
68
+ ## Build
69
+
70
+ ```bash
71
+ uv build
72
+ ```
73
+
74
+ The distributions are created in `dist/`.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pythonai-grounded-generator"
7
+ version = "0.1.0"
8
+ description = "Lightweight grounded text generation from retrieved context"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "torch>=2.2",
13
+ "transformers>=4.51.0",
14
+ ]
15
+ keywords = ["llm", "rag", "grounded-generation", "transformers", "qwen"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ ]
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+
26
+ [tool.setuptools.package-data]
27
+ pythonai_grounded = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """Helpers for grounded generation with an already loaded causal language model."""
2
+
3
+ from .core import generate_answer
4
+
5
+ __all__ = ["generate_answer"]
@@ -0,0 +1,91 @@
1
+ """Grounded answer generation with configurable prompts and token budgets."""
2
+
3
+ from typing import Any
4
+
5
+ import torch
6
+
7
+
8
+ def generate_answer(
9
+ model: Any,
10
+ tokenizer: Any,
11
+ question: str,
12
+ context: str,
13
+ system_prompt: str,
14
+ max_new_tokens: int = 64,
15
+ device: str = "cpu",
16
+ ) -> str:
17
+ """Generate an answer using a preloaded model and retrieved context.
18
+
19
+ The caller owns retrieval, augmentation, model loading, and output schema.
20
+ This function formats the prompt, truncates context to the token budget,
21
+ generates an answer, and returns only the decoded completion.
22
+
23
+ Raises:
24
+ ValueError: If inputs are invalid or the prompt exceeds the budget.
25
+ """
26
+ if not question.strip() or not system_prompt.strip():
27
+ raise ValueError("Question and system prompt must not be empty")
28
+ if max_new_tokens <= 0:
29
+ raise ValueError("max_new_tokens must be positive")
30
+
31
+ user_prefix = "Retrieved context:\n"
32
+ user_suffix = (
33
+ f"\n\nQuestion:\n{question}\n\n"
34
+ "Important: copy technical identifiers exactly as written "
35
+ "in the retrieved context."
36
+ )
37
+ messages = [
38
+ {"role": "system", "content": system_prompt},
39
+ {"role": "user", "content": user_prefix + user_suffix},
40
+ ]
41
+
42
+ # Count tokens needed even when the retrieved context is empty.
43
+ fixed_ids = tokenizer.apply_chat_template(
44
+ messages,
45
+ tokenize=True,
46
+ return_dict=False,
47
+ add_generation_prompt=True,
48
+ enable_thinking=False,
49
+ )
50
+ model_limit = getattr(model.config, "max_position_embeddings", 32768)
51
+ budget = model_limit - len(fixed_ids) - max_new_tokens - 32
52
+ if budget <= 0:
53
+ raise ValueError("Not enough token budget for the prompt")
54
+
55
+ context_ids = tokenizer(
56
+ context,
57
+ add_special_tokens=False,
58
+ truncation=True,
59
+ max_length=budget,
60
+ )["input_ids"]
61
+ trimmed_context = tokenizer.decode(context_ids, skip_special_tokens=True)
62
+ messages[1]["content"] = user_prefix + trimmed_context + user_suffix
63
+
64
+ prompt = tokenizer.apply_chat_template(
65
+ messages,
66
+ tokenize=False,
67
+ add_generation_prompt=True,
68
+ enable_thinking=False,
69
+ )
70
+ inputs = tokenizer(
71
+ prompt,
72
+ return_tensors="pt",
73
+ add_special_tokens=False,
74
+ ).to(device)
75
+ input_length = inputs["input_ids"].shape[1]
76
+ if input_length + max_new_tokens > model_limit:
77
+ raise ValueError("Prompt exceeds the model token budget")
78
+
79
+ with torch.inference_mode():
80
+ output = model.generate(
81
+ **inputs,
82
+ max_new_tokens=max_new_tokens,
83
+ do_sample=False,
84
+ eos_token_id=tokenizer.eos_token_id,
85
+ pad_token_id=tokenizer.eos_token_id,
86
+ )
87
+
88
+ return tokenizer.decode(
89
+ output[0][input_length:],
90
+ skip_special_tokens=True,
91
+ ).strip()
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonai-grounded-generator
3
+ Version: 0.1.0
4
+ Summary: Lightweight grounded text generation from retrieved context
5
+ Keywords: llm,rag,grounded-generation,transformers,qwen
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: torch>=2.2
13
+ Requires-Dist: transformers>=4.51.0
14
+
15
+ # PythonAI Grounded Generator
16
+
17
+ A compact Python helper for generating answers from retrieved context with an already loaded Hugging Face causal language model.
18
+
19
+ It provides:
20
+
21
+ - Custom system prompts and questions
22
+ - Token-budget-aware context truncation
23
+ - Qwen3 non-thinking chat formatting
24
+ - CPU or CUDA inference (device controlled by the caller)
25
+ - A plain-text answer suitable for wrapping in application-specific Pydantic models
26
+
27
+ Retrieval, context assembly, and structured JSON serialization remain the responsibility of the calling application. This package does **not** guarantee factual correctness or replace source validation.
28
+
29
+ ## Installation
30
+
31
+ After publication:
32
+
33
+ ```bash
34
+ pip install pythonai-grounded-generator
35
+ ```
36
+
37
+ Or install the local project:
38
+
39
+ ```bash
40
+ pip install .
41
+ ```
42
+
43
+ ## Example
44
+
45
+ ```python
46
+ from transformers import AutoModelForCausalLM, AutoTokenizer
47
+ from pythonai_grounded import generate_answer
48
+
49
+ name = "Qwen/Qwen3-0.6B"
50
+ tokenizer = AutoTokenizer.from_pretrained(name)
51
+ model = AutoModelForCausalLM.from_pretrained(name).to("cpu")
52
+ model.eval()
53
+
54
+ answer = generate_answer(
55
+ model=model,
56
+ tokenizer=tokenizer,
57
+ question="Which endpoint loads an adapter?",
58
+ context="Use POST /v1/load_lora_adapter to load an adapter.",
59
+ system_prompt="Answer only from the context. Copy technical names exactly.",
60
+ max_new_tokens=64,
61
+ device="cpu",
62
+ )
63
+ print(answer)
64
+ ```
65
+
66
+ ## API
67
+
68
+ ```python
69
+ generate_answer(
70
+ model,
71
+ tokenizer,
72
+ question: str,
73
+ context: str,
74
+ system_prompt: str,
75
+ max_new_tokens: int = 64,
76
+ device: str = "cpu",
77
+ ) -> str
78
+ ```
79
+
80
+ The supplied `model` and `tokenizer` must be compatible. The model must already be on the requested device and in evaluation mode. Model context limits are read from `model.config.max_position_embeddings`, falling back to 32768 if absent. Check actual model capabilities when switching models.
81
+
82
+ ## Build
83
+
84
+ ```bash
85
+ uv build
86
+ ```
87
+
88
+ The distributions are created in `dist/`.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/pythonai_grounded/__init__.py
4
+ src/pythonai_grounded/core.py
5
+ src/pythonai_grounded/py.typed
6
+ src/pythonai_grounded_generator.egg-info/PKG-INFO
7
+ src/pythonai_grounded_generator.egg-info/SOURCES.txt
8
+ src/pythonai_grounded_generator.egg-info/dependency_links.txt
9
+ src/pythonai_grounded_generator.egg-info/requires.txt
10
+ src/pythonai_grounded_generator.egg-info/top_level.txt
11
+ tests/test_core.py
@@ -0,0 +1,2 @@
1
+ torch>=2.2
2
+ transformers>=4.51.0
@@ -0,0 +1,95 @@
1
+ """Unit tests using fake tokenizer and model: no model download needed."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+ import torch
7
+
8
+ from pythonai_grounded import generate_answer
9
+
10
+
11
+ class TensorInputs(dict):
12
+ def to(self, device):
13
+ return self
14
+
15
+
16
+ class DummyTokenizer:
17
+ eos_token_id = 2
18
+
19
+ def __init__(self, final_length=8):
20
+ self.final_length = final_length
21
+ self.last_context_ids = None
22
+ self.last_messages = None
23
+
24
+ def apply_chat_template(self, messages, tokenize, **kwargs):
25
+ self.last_messages = [dict(message) for message in messages]
26
+ assert kwargs["enable_thinking"] is False
27
+ if tokenize:
28
+ return [1, 2, 3, 4]
29
+ return "\n".join(message["content"] for message in messages)
30
+
31
+ def __call__(self, text, **kwargs):
32
+ if kwargs.get("return_tensors") == "pt":
33
+ length = self.final_length
34
+ return TensorInputs({
35
+ "input_ids": torch.ones((1, length), dtype=torch.long),
36
+ "attention_mask": torch.ones((1, length), dtype=torch.long),
37
+ })
38
+ max_length = kwargs["max_length"]
39
+ self.last_context_ids = list(range(min(len(text.split()), max_length)))
40
+ return {"input_ids": self.last_context_ids}
41
+
42
+ def decode(self, ids, **kwargs):
43
+ ids = ids.tolist() if hasattr(ids, "tolist") else ids
44
+ return "GENERATED ANSWER" if 999 in ids else "context"
45
+
46
+
47
+ class DummyModel:
48
+ def __init__(self, limit=200):
49
+ self.config = SimpleNamespace(max_position_embeddings=limit)
50
+ self.last_inputs = None
51
+
52
+ def generate(self, **kwargs):
53
+ self.last_inputs = kwargs
54
+ return torch.cat(
55
+ [kwargs["input_ids"], torch.tensor([[999]], dtype=torch.long)],
56
+ dim=1,
57
+ )
58
+
59
+
60
+ def test_basic_generation_and_prompt():
61
+ model = DummyModel()
62
+ tokenizer = DummyTokenizer()
63
+ output = generate_answer(model, tokenizer, "What?", "evidence here", "Use evidence")
64
+ assert output == "GENERATED ANSWER"
65
+ assert tokenizer.last_messages[0]["content"] == "Use evidence"
66
+ assert "Question:\nWhat?" in tokenizer.last_messages[1]["content"]
67
+ assert model.last_inputs["do_sample"] is False
68
+
69
+
70
+ def test_context_is_truncated():
71
+ model = DummyModel(limit=50)
72
+ tokenizer = DummyTokenizer()
73
+ generate_answer(model, tokenizer, "Question?", "word " * 100, "Instruction", 5)
74
+ assert len(tokenizer.last_context_ids) == 9 # 50 - 4 - 5 - 32
75
+
76
+
77
+ @pytest.mark.parametrize("question,prompt,tokens", [
78
+ ("", "Instruction", 64),
79
+ ("Question", "", 64),
80
+ ("Question", "Instruction", 0),
81
+ ])
82
+ def test_invalid_inputs(question, prompt, tokens):
83
+ with pytest.raises(ValueError):
84
+ generate_answer(DummyModel(), DummyTokenizer(), question, "context", prompt, tokens)
85
+
86
+
87
+ def test_insufficient_budget():
88
+ with pytest.raises(ValueError, match="Not enough token budget"):
89
+ generate_answer(DummyModel(limit=30), DummyTokenizer(), "Q", "text", "S")
90
+
91
+
92
+ def test_final_length_check():
93
+ tokenizer = DummyTokenizer(final_length=100)
94
+ with pytest.raises(ValueError, match="exceeds"):
95
+ generate_answer(DummyModel(limit=90), tokenizer, "Q", "text", "S", 5)