wuenlp_tools.models.sentiment.SentimentPromptingLLM
1from dataclasses import dataclass, field 2import json 3from typing import Type 4 5from loguru import logger 6from pydantic import BaseModel 7from tqdm.auto import tqdm 8 9from wuenlp_tools.keys import OPENAI_API_KEY 10from wuenlp_tools.pipeline import PipelineStep, PipelineProcessor 11from wuenlp_tools.utils.prompting import LLMArchitecture, default_llm, LLM 12from wuenlp.impl.UIMANLPStructs import UIMADocument, UIMAAnnotation, UIMASpan, UIMASystemScene 13 14 15class FloatSentimentOutput(BaseModel): 16 sentiment: float 17 18 19DEFAULT_BASE_LLM_SYSTEM_PROMPT = ( 20 "You are given a piece of text from a novel. You should determine the sentiment of the text on a scale " 21 "from -1 to 1, depending on how well the overall situation described in the text is. Return a valid json " 22 "object of the required format." 23) 24 25 26@dataclass(frozen=True) 27class BaseLLMSentimentConfig: 28 model: LLMArchitecture = field(default_factory=lambda: default_llm) 29 system_prompt: str = DEFAULT_BASE_LLM_SYSTEM_PROMPT 30 base_url: str = "https://ollama.professor-x.de/v1/" 31 feature_key: str = "llama_sentiment" 32 33 34class BaseLLMSentimentModel(object): 35 model: LLM 36 37 def __init__(self, config: BaseLLMSentimentConfig | None = None, output_format: type[BaseModel] = FloatSentimentOutput): 38 cfg = config or BaseLLMSentimentConfig() 39 self.model = LLM( 40 model=cfg.model, 41 system_prompt=cfg.system_prompt, 42 output_format=output_format, 43 base_url=cfg.base_url, 44 ) 45 46 @staticmethod 47 def normalize_sentiment(sentiment): 48 return min(1, max(-1, sentiment)) 49 50 def __call__(self, text, output_type: Type = float): 51 assert output_type in (float,), "Only float output_type is supported for GPTSentimentModel" 52 response = self.model(text) 53 if hasattr(response, "sentiment"): 54 return self.normalize_sentiment(response.sentiment) 55 56 if isinstance(response, str): 57 try: 58 parsed = json.loads(response) 59 if isinstance(parsed, dict) and "sentiment" in parsed: 60 return self.normalize_sentiment(float(parsed["sentiment"])) 61 except Exception: 62 try: 63 return self.normalize_sentiment(float(response)) 64 except Exception: 65 pass 66 67 raise ValueError(f"Unexpected sentiment response type: {type(response).__name__}") 68 69 70def base_llm_sentiment( 71 doc: UIMADocument, 72 unit_type: Type[UIMASpan], 73 overwrite: bool = False, 74 config: BaseLLMSentimentConfig | None = None, 75) -> UIMADocument: 76 cfg = config or BaseLLMSentimentConfig() 77 if not overwrite and any( 78 cfg.feature_key in segment.additional_features for segment in doc._get_annos_of_type(unit_type)): 79 logger.warning( 80 f"Skipping {unit_type} sentiment prediction because some segments already have a sentiment prediction") 81 return doc 82 model = BaseLLMSentimentModel(config=cfg) 83 segments = doc._get_annos_of_type(unit_type) 84 progress_desc = f"LLM sentiment ({unit_type.__name__})" 85 for segment in tqdm(segments, desc=progress_desc, unit="segment"): 86 try: 87 segment.additional_features[cfg.feature_key] = model(segment.text) 88 except Exception as exc: 89 logger.warning(f"Falling back to neutral sentiment due to model error: {exc}") 90 segment.additional_features[cfg.feature_key] = 0.0 91 return doc 92 93 94class BaseLLMSentimentProcessor(PipelineProcessor): 95 def __init__(self, config: BaseLLMSentimentConfig | None = None): 96 self.config = config or BaseLLMSentimentConfig() 97 98 def __call__(self, doc: UIMADocument, unit_type: Type[UIMASpan], overwrite: bool = False, **kwargs) -> UIMADocument: 99 return base_llm_sentiment(doc, unit_type, overwrite=overwrite, config=self.config) 100 101 102def BaseLLMSentimentStep( 103 config: BaseLLMSentimentConfig | None = None, 104 unit_type: Type[UIMAAnnotation] = UIMASystemScene, 105 name: str = "LLM Sentiment", 106) -> PipelineStep[BaseLLMSentimentProcessor]: 107 cfg = config or BaseLLMSentimentConfig() 108 return PipelineStep[BaseLLMSentimentProcessor]( 109 name, 110 BaseLLMSentimentProcessor(config=cfg), 111 unit_type=unit_type, 112 added_additional_features=[cfg.feature_key], 113 requires_api_key=OPENAI_API_KEY, 114 requires_paid_api_requests=True, 115 ) 116 117 118BaseLLMSentiment = BaseLLMSentimentStep()
!!! abstract "Usage Documentation" Models
A base class for creating Pydantic models.
Attributes:
__class_vars__: The names of the class variables defined on the model.
__private_attributes__: Metadata about the private attributes of the model.
__signature__: The synthesized __init__ [Signature][inspect.Signature] of the model.
__pydantic_complete__: Whether model building is completed, or if there are still undefined fields.
__pydantic_core_schema__: The core schema of the model.
__pydantic_custom_init__: Whether the model has a custom `__init__` function.
__pydantic_decorators__: Metadata containing the decorators defined on the model.
This replaces `Model.__validators__` and `Model.__root_validators__` from Pydantic V1.
__pydantic_generic_metadata__: A dictionary containing metadata about generic Pydantic models.
The `origin` and `args` items map to the [`__origin__`][genericalias.__origin__]
and [`__args__`][genericalias.__args__] attributes of [generic aliases][types-genericalias],
and the `parameter` item maps to the `__parameter__` attribute of generic classes.
__pydantic_parent_namespace__: Parent namespace of the model, used for automatic rebuilding of models.
__pydantic_post_init__: The name of the post-init method for the model, if defined.
__pydantic_root_model__: Whether the model is a [`RootModel`][pydantic.root_model.RootModel].
__pydantic_serializer__: The `pydantic-core` `SchemaSerializer` used to dump instances of the model.
__pydantic_validator__: The `pydantic-core` `SchemaValidator` used to validate instances of the model.
__pydantic_fields__: A dictionary of field names and their corresponding [`FieldInfo`][pydantic.fields.FieldInfo] objects.
__pydantic_computed_fields__: A dictionary of computed field names and their corresponding [`ComputedFieldInfo`][pydantic.fields.ComputedFieldInfo] objects.
__pydantic_extra__: A dictionary containing extra values, if [`extra`][pydantic.config.ConfigDict.extra]
is set to `'allow'`.
__pydantic_fields_set__: The names of fields explicitly set during instantiation.
__pydantic_private__: Values of private attributes set on the model instance.
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
27@dataclass(frozen=True) 28class BaseLLMSentimentConfig: 29 model: LLMArchitecture = field(default_factory=lambda: default_llm) 30 system_prompt: str = DEFAULT_BASE_LLM_SYSTEM_PROMPT 31 base_url: str = "https://ollama.professor-x.de/v1/" 32 feature_key: str = "llama_sentiment"
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
35class BaseLLMSentimentModel(object): 36 model: LLM 37 38 def __init__(self, config: BaseLLMSentimentConfig | None = None, output_format: type[BaseModel] = FloatSentimentOutput): 39 cfg = config or BaseLLMSentimentConfig() 40 self.model = LLM( 41 model=cfg.model, 42 system_prompt=cfg.system_prompt, 43 output_format=output_format, 44 base_url=cfg.base_url, 45 ) 46 47 @staticmethod 48 def normalize_sentiment(sentiment): 49 return min(1, max(-1, sentiment)) 50 51 def __call__(self, text, output_type: Type = float): 52 assert output_type in (float,), "Only float output_type is supported for GPTSentimentModel" 53 response = self.model(text) 54 if hasattr(response, "sentiment"): 55 return self.normalize_sentiment(response.sentiment) 56 57 if isinstance(response, str): 58 try: 59 parsed = json.loads(response) 60 if isinstance(parsed, dict) and "sentiment" in parsed: 61 return self.normalize_sentiment(float(parsed["sentiment"])) 62 except Exception: 63 try: 64 return self.normalize_sentiment(float(response)) 65 except Exception: 66 pass 67 68 raise ValueError(f"Unexpected sentiment response type: {type(response).__name__}")
38 def __init__(self, config: BaseLLMSentimentConfig | None = None, output_format: type[BaseModel] = FloatSentimentOutput): 39 cfg = config or BaseLLMSentimentConfig() 40 self.model = LLM( 41 model=cfg.model, 42 system_prompt=cfg.system_prompt, 43 output_format=output_format, 44 base_url=cfg.base_url, 45 )
71def base_llm_sentiment( 72 doc: UIMADocument, 73 unit_type: Type[UIMASpan], 74 overwrite: bool = False, 75 config: BaseLLMSentimentConfig | None = None, 76) -> UIMADocument: 77 cfg = config or BaseLLMSentimentConfig() 78 if not overwrite and any( 79 cfg.feature_key in segment.additional_features for segment in doc._get_annos_of_type(unit_type)): 80 logger.warning( 81 f"Skipping {unit_type} sentiment prediction because some segments already have a sentiment prediction") 82 return doc 83 model = BaseLLMSentimentModel(config=cfg) 84 segments = doc._get_annos_of_type(unit_type) 85 progress_desc = f"LLM sentiment ({unit_type.__name__})" 86 for segment in tqdm(segments, desc=progress_desc, unit="segment"): 87 try: 88 segment.additional_features[cfg.feature_key] = model(segment.text) 89 except Exception as exc: 90 logger.warning(f"Falling back to neutral sentiment due to model error: {exc}") 91 segment.additional_features[cfg.feature_key] = 0.0 92 return doc
95class BaseLLMSentimentProcessor(PipelineProcessor): 96 def __init__(self, config: BaseLLMSentimentConfig | None = None): 97 self.config = config or BaseLLMSentimentConfig() 98 99 def __call__(self, doc: UIMADocument, unit_type: Type[UIMASpan], overwrite: bool = False, **kwargs) -> UIMADocument: 100 return base_llm_sentiment(doc, unit_type, overwrite=overwrite, config=self.config)
Base class for protocol classes.
Protocol classes are defined as::
class Proto(Protocol):
def meth(self) -> int:
...
Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing).
For example::
class C:
def meth(self) -> int:
return 0
def func(x: Proto) -> int:
return x.meth()
func(C()) # Passes static type check
See PEP 544 for details. Protocol classes decorated with @typing.runtime_checkable act as simple-minded runtime protocols that check only the presence of given attributes, ignoring their type signatures. Protocol classes can be generic, they are defined as::
class GenProto[T](Protocol):
def meth(self) -> T:
...
103def BaseLLMSentimentStep( 104 config: BaseLLMSentimentConfig | None = None, 105 unit_type: Type[UIMAAnnotation] = UIMASystemScene, 106 name: str = "LLM Sentiment", 107) -> PipelineStep[BaseLLMSentimentProcessor]: 108 cfg = config or BaseLLMSentimentConfig() 109 return PipelineStep[BaseLLMSentimentProcessor]( 110 name, 111 BaseLLMSentimentProcessor(config=cfg), 112 unit_type=unit_type, 113 added_additional_features=[cfg.feature_key], 114 requires_api_key=OPENAI_API_KEY, 115 requires_paid_api_requests=True, 116 )
Pipeline step LLM Sentiment (BaseLLMSentimentProcessor).
unit type UIMASystemScene; additional features llama_sentiment.