google--langextract
76d991c447
Auto Update PR / update-prs (push) Has been cancelled
CI / format-check (push) Has been cancelled
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / live-api-tests (push) Has been cancelled
CI / plugin-integration-test (push) Has been cancelled
CI / ollama-integration-test (push) Has been cancelled
CI / test-fork-pr (push) Has been cancelled
277 行
8.4 KiB
Python
277 行
8.4 KiB
Python
# Copyright 2025 Google LLC.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Library for building prompts."""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import json
|
|
import pathlib
|
|
|
|
import pydantic
|
|
from typing_extensions import override
|
|
import yaml
|
|
|
|
from langextract.core import data
|
|
from langextract.core import exceptions
|
|
from langextract.core import format_handler
|
|
|
|
|
|
class PromptBuilderError(exceptions.LangExtractError):
|
|
"""Failure to build prompt."""
|
|
|
|
|
|
class ParseError(PromptBuilderError):
|
|
"""Prompt template cannot be parsed."""
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class PromptTemplateStructured:
|
|
"""A structured prompt template for few-shot examples.
|
|
|
|
Attributes:
|
|
description: Instructions or guidelines for the LLM.
|
|
examples: ExampleData objects demonstrating expected input→output behavior.
|
|
"""
|
|
|
|
description: str
|
|
examples: list[data.ExampleData] = dataclasses.field(default_factory=list)
|
|
|
|
|
|
def read_prompt_template_structured_from_file(
|
|
prompt_path: str,
|
|
format_type: data.FormatType = data.FormatType.YAML,
|
|
) -> PromptTemplateStructured:
|
|
"""Reads a structured prompt template from a file.
|
|
|
|
Args:
|
|
prompt_path: Path to a file containing PromptTemplateStructured data.
|
|
format_type: The format of the file; YAML or JSON.
|
|
|
|
Returns:
|
|
A PromptTemplateStructured object loaded from the file.
|
|
|
|
Raises:
|
|
ParseError: If the file cannot be parsed successfully.
|
|
"""
|
|
adapter = pydantic.TypeAdapter(PromptTemplateStructured)
|
|
try:
|
|
with pathlib.Path(prompt_path).open("rt") as f:
|
|
data_dict = {}
|
|
prompt_content = f.read()
|
|
if format_type == data.FormatType.YAML:
|
|
data_dict = yaml.safe_load(prompt_content)
|
|
elif format_type == data.FormatType.JSON:
|
|
data_dict = json.loads(prompt_content)
|
|
return adapter.validate_python(data_dict)
|
|
except Exception as e:
|
|
raise ParseError(
|
|
f"Failed to parse prompt template from file: {prompt_path}"
|
|
) from e
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class QAPromptGenerator:
|
|
"""Generates question-answer prompts from the provided template."""
|
|
|
|
template: PromptTemplateStructured
|
|
format_handler: format_handler.FormatHandler
|
|
examples_heading: str = "Examples"
|
|
question_prefix: str = "Q: "
|
|
answer_prefix: str = "A: "
|
|
|
|
def __str__(self) -> str:
|
|
"""Returns a string representation of the prompt with an empty question."""
|
|
return self.render("")
|
|
|
|
def format_example_as_text(self, example: data.ExampleData) -> str:
|
|
"""Formats a single example for the prompt.
|
|
|
|
Args:
|
|
example: The example data to format.
|
|
|
|
Returns:
|
|
A string representation of the example, including the question and answer.
|
|
"""
|
|
question = example.text
|
|
answer = self.format_handler.format_extraction_example(example.extractions)
|
|
|
|
return "\n".join([
|
|
f"{self.question_prefix}{question}",
|
|
f"{self.answer_prefix}{answer}\n",
|
|
])
|
|
|
|
def render(self, question: str, additional_context: str | None = None) -> str:
|
|
"""Generate a text representation of the prompt.
|
|
|
|
Args:
|
|
question: That will be presented to the model.
|
|
additional_context: Additional context to include in the prompt. An empty
|
|
string is ignored.
|
|
|
|
Returns:
|
|
Text prompt with a question to be presented to a language model.
|
|
"""
|
|
prompt_lines: list[str] = [f"{self.template.description}\n"]
|
|
|
|
if additional_context:
|
|
prompt_lines.append(f"{additional_context}\n")
|
|
|
|
if self.template.examples:
|
|
prompt_lines.append(self.examples_heading)
|
|
for ex in self.template.examples:
|
|
prompt_lines.append(self.format_example_as_text(ex))
|
|
|
|
prompt_lines.append(f"{self.question_prefix}{question}")
|
|
prompt_lines.append(self.answer_prefix)
|
|
return "\n".join(prompt_lines)
|
|
|
|
|
|
class PromptBuilder:
|
|
"""Builds prompts for text chunks using a QAPromptGenerator.
|
|
|
|
This base class provides a simple interface for prompt generation. Subclasses
|
|
can extend this to add stateful behavior like cross-chunk context tracking.
|
|
"""
|
|
|
|
def __init__(self, generator: QAPromptGenerator):
|
|
"""Initializes the builder with the given prompt generator.
|
|
|
|
Args:
|
|
generator: The underlying prompt generator to use.
|
|
"""
|
|
self._generator = generator
|
|
|
|
def build_prompt(
|
|
self,
|
|
chunk_text: str,
|
|
document_id: str,
|
|
additional_context: str | None = None,
|
|
) -> str:
|
|
"""Builds a prompt for the given chunk.
|
|
|
|
Args:
|
|
chunk_text: The text of the current chunk to process.
|
|
document_id: Identifier for the source document.
|
|
additional_context: Optional additional context from the document.
|
|
|
|
Returns:
|
|
The rendered prompt string ready for the language model.
|
|
"""
|
|
del document_id # Unused in base class.
|
|
return self._generator.render(
|
|
question=chunk_text,
|
|
additional_context=additional_context,
|
|
)
|
|
|
|
|
|
class ContextAwarePromptBuilder(PromptBuilder):
|
|
"""Prompt builder with cross-chunk context tracking.
|
|
|
|
Extends PromptBuilder to inject text from the previous chunk into each
|
|
prompt. This helps language models resolve coreferences across chunk
|
|
boundaries (e.g., connecting "She" to "Dr. Sarah Johnson" from the
|
|
previous chunk).
|
|
|
|
Context is tracked per document_id, so multiple documents can be processed
|
|
without context bleeding between them.
|
|
"""
|
|
|
|
_CONTEXT_PREFIX = "[Previous text]: ..."
|
|
|
|
def __init__(
|
|
self,
|
|
generator: QAPromptGenerator,
|
|
context_window_chars: int | None = None,
|
|
):
|
|
"""Initializes the builder with context tracking configuration.
|
|
|
|
Args:
|
|
generator: The underlying prompt generator to use.
|
|
context_window_chars: Number of characters from the previous chunk's
|
|
tail to include as context. Defaults to None (disabled).
|
|
"""
|
|
super().__init__(generator)
|
|
self._context_window_chars = context_window_chars
|
|
self._prev_chunk_by_doc_id: dict[str, str] = {}
|
|
|
|
@property
|
|
def context_window_chars(self) -> int | None:
|
|
"""Number of trailing characters from previous chunk to include."""
|
|
return self._context_window_chars
|
|
|
|
@override
|
|
def build_prompt(
|
|
self,
|
|
chunk_text: str,
|
|
document_id: str,
|
|
additional_context: str | None = None,
|
|
) -> str:
|
|
"""Builds a prompt, injecting previous chunk context if enabled.
|
|
|
|
Args:
|
|
chunk_text: The text of the current chunk to process.
|
|
document_id: Identifier for the source document (used to track context
|
|
per document).
|
|
additional_context: Optional additional context from the document.
|
|
|
|
Returns:
|
|
The rendered prompt string ready for the language model.
|
|
"""
|
|
effective_context = self._build_effective_context(
|
|
document_id, additional_context
|
|
)
|
|
prompt = self._generator.render(
|
|
question=chunk_text,
|
|
additional_context=effective_context,
|
|
)
|
|
self._update_state(document_id, chunk_text)
|
|
return prompt
|
|
|
|
def _build_effective_context(
|
|
self,
|
|
document_id: str,
|
|
additional_context: str | None,
|
|
) -> str | None:
|
|
"""Combines previous chunk context with any additional context.
|
|
|
|
Args:
|
|
document_id: Identifier for the source document.
|
|
additional_context: Optional additional context from the document.
|
|
|
|
Returns:
|
|
Combined context string, or None if no context is available.
|
|
"""
|
|
context_parts: list[str] = []
|
|
|
|
if self._context_window_chars and document_id in self._prev_chunk_by_doc_id:
|
|
prev_text = self._prev_chunk_by_doc_id[document_id]
|
|
window = prev_text[-self._context_window_chars :]
|
|
context_parts.append(f"{self._CONTEXT_PREFIX}{window}")
|
|
|
|
if additional_context:
|
|
context_parts.append(additional_context)
|
|
|
|
return "\n\n".join(context_parts) if context_parts else None
|
|
|
|
def _update_state(self, document_id: str, chunk_text: str) -> None:
|
|
"""Stores current chunk as context for the next chunk in this document.
|
|
|
|
Args:
|
|
document_id: Identifier for the source document.
|
|
chunk_text: The current chunk text to store.
|
|
"""
|
|
if self._context_window_chars:
|
|
self._prev_chunk_by_doc_id[document_id] = chunk_text
|