项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:37:14 +08:00

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