llm-ie 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.
llm_ie/__init__.py ADDED
File without changes
llm_ie/data_types.py ADDED
@@ -0,0 +1,167 @@
1
+ from typing import List, Dict
2
+ import yaml
3
+
4
+
5
+ class LLMInformationExtractionFrame:
6
+ def __init__(self, frame_id:str, start:int, end:int, entity_text:str, attr:Dict[str,str]):
7
+ """
8
+ This class holds a frame (entity) extracted by LLM.
9
+ A frame contains the span (start and end character positions), a entity text, and
10
+ a set of attributes.
11
+
12
+ Parameters
13
+ ----------
14
+ frame_id : str
15
+ unique identiifier for the entity
16
+ start : int
17
+ entity start character position
18
+ end : int
19
+ entity end character position
20
+ entity_text : str
21
+ entity string. Should be the exact string by [start:end]
22
+ attr : Dict[str,str]
23
+ dict of attributes
24
+ """
25
+ assert isinstance(frame_id, str), "frame_id must be a string."
26
+ self.frame_id = frame_id
27
+ self.start = start
28
+ self.end = end
29
+ self.entity_text = entity_text
30
+ self.attr = attr.copy()
31
+
32
+ def is_equal(self, frame:"LLMInformationExtractionFrame") -> bool:
33
+ """
34
+ This method checks if an external frame holds the same information as self.
35
+ This can be used in evaluation against gold standard.
36
+ """
37
+ return self.start == frame.start and self.end == frame.end
38
+
39
+ def is_overlap(self, frame:"LLMInformationExtractionFrame") -> bool:
40
+ """
41
+ This method checks if an external frame overlaps with self.
42
+ This can be used in evaluation against gold standard.
43
+ """
44
+ if self.end < frame.start or self.start > frame.end:
45
+ return False
46
+ return True
47
+
48
+ def to_dict(self) -> Dict[str,str]:
49
+ """
50
+ This method outputs the frame contents to a dictionary.
51
+ """
52
+ return {"frame_id": self.frame_id,
53
+ "start": self.start,
54
+ "end": self.end,
55
+ "entity_text": self.entity_text,
56
+ "attr": self.attr}
57
+
58
+ @classmethod
59
+ def from_dict(cls, d: Dict[str,str]) -> "LLMInformationExtractionFrame":
60
+ """
61
+ This method defines a LLMInformationExtractionFrame from dictionary.
62
+ """
63
+ return cls(frame_id=d['frame_id'],
64
+ start=d['start'],
65
+ end=d['end'],
66
+ entity_text=d['entity_text'],
67
+ attr=d['attr'])
68
+
69
+ def copy(self) -> "LLMInformationExtractionFrame":
70
+ return LLMInformationExtractionFrame(frame_id=self.frame_id,
71
+ start=self.start,
72
+ end=self.end,
73
+ entity_text=self.entity_text,
74
+ attr=self.attr)
75
+
76
+
77
+ class LLMInformationExtractionDocument:
78
+ def __init__(self, doc_id:str=None, filename:str=None, text:str=None, frames:List[LLMInformationExtractionFrame]=None):
79
+ """
80
+ This class holds LLM-extracted frames, handles save/ load.
81
+
82
+ Parameters
83
+ ----------
84
+ doc_id : str, Optional
85
+ document ID. Must be a string
86
+ filename : str, Optional
87
+ the directory to a yaml file of a saved LLMInformationExtractionDocument
88
+ text : str, Optional
89
+ document text
90
+ frames : List[LLMInformationExtractionFrame], Optional
91
+ a list of LLMInformationExtractionFrame
92
+ """
93
+ assert doc_id or filename, "Either doc_id (create from raw inputs) or filename (create from file) must be provided."
94
+ # if create object from file
95
+ if filename:
96
+ with open(filename) as yaml_file:
97
+ llm_ie = yaml.safe_load(yaml_file)
98
+ if 'doc_id' in llm_ie.keys():
99
+ self.doc_id = llm_ie['doc_id']
100
+ if 'text' in llm_ie.keys():
101
+ self.text = llm_ie['text']
102
+ if 'frames' in llm_ie.keys():
103
+ self.frames = [LLMInformationExtractionFrame.from_dict(d) for d in llm_ie['frames']]
104
+
105
+ # create object from raw inputs
106
+ else:
107
+ assert isinstance(doc_id, str), "doc_id must be a string."
108
+ self.doc_id = doc_id
109
+ self.text = text
110
+ self.frames = frames.copy() if frames is not None else []
111
+
112
+
113
+ def has_frame(self) -> bool:
114
+ return bool(self.frames)
115
+
116
+ def add_frame(self, frame:LLMInformationExtractionFrame, valid_mode:str=None, create_id:bool=False) -> bool:
117
+ """
118
+ This method add a new frame to the frames (list).
119
+
120
+ Parameters
121
+ ----------
122
+ frame : LLMInformationExtractionFrame
123
+ the new frame to add.
124
+ valid_mode : str, Optional
125
+ one of {None, "span", "attr"}
126
+ if None, no validation will be done, add frame.
127
+ if "span", if the new frame's span is equal to an existing frame, add will fail.
128
+ if "attr", if the new frame's span and all attributes is equal to an existing frame, add will fail.
129
+ create_id : bool, Optional
130
+ Assign a sequential frame ID.
131
+ """
132
+ assert valid_mode in {None, "span", "attr"}, 'valid_mode must be one of {None, "span", "attr"}'
133
+
134
+ if valid_mode == "span":
135
+ for exist_frame in self.frames:
136
+ if exist_frame.is_equal(frame):
137
+ return False
138
+
139
+ if valid_mode == "attr":
140
+ for exist_frame in self.frames:
141
+ if exist_frame.is_equal(frame) and exist_frame.attr == frame.attr:
142
+ return False
143
+
144
+ # Add frame
145
+ frame_clone = frame.copy()
146
+ if create_id:
147
+ frame_clone.doc_id = f"{self.doc_id}_{len(self.frames)}"
148
+
149
+ self.frames.append(frame_clone)
150
+ return True
151
+
152
+
153
+ def __repr__(self, N_top_chars:int=100) -> str:
154
+ text_to_print = self.text[0:N_top_chars]
155
+ frame_count = len(self.frames)
156
+ return ''.join((f'LLMInformationExtractionDocument(doc_id="{self.doc_id}...")\n',
157
+ f'text="{text_to_print}...",\n',
158
+ f'frames={frame_count}'))
159
+
160
+ def save(self, filename:str):
161
+ with open(filename, 'w') as yaml_file:
162
+ yaml.safe_dump({'doc_id':self.doc_id,
163
+ 'text':self.text,
164
+ 'frames':[frame.to_dict() for frame in self.frames]},
165
+ yaml_file, sort_keys=False)
166
+ yaml_file.flush()
167
+
llm_ie/engines.py ADDED
@@ -0,0 +1,166 @@
1
+ import abc
2
+ from typing import List, Dict
3
+
4
+ class InferenceEngine:
5
+ @abc.abstractmethod
6
+ def __init__(self):
7
+ """
8
+ This is an abstract class to provide interfaces for LLM inference engines.
9
+ Children classes that inherts this class can be used in extrators. Must implement chat() method.
10
+ """
11
+ return NotImplemented
12
+
13
+
14
+ @abc.abstractmethod
15
+ def chat(self, messages:List[Dict[str,str]], max_new_tokens:int=2048, temperature:float=0.0, stream:bool=False, **kwrs) -> str:
16
+ """
17
+ This method inputs chat messages and outputs LLM generated text.
18
+
19
+ Parameters:
20
+ ----------
21
+ messages : List[Dict[str,str]]
22
+ a list of dict with role and content. role must be one of {"system", "user", "assistant"}
23
+ max_new_tokens : str, Optional
24
+ the max number of new tokens LLM can generate.
25
+ temperature : float, Optional
26
+ the temperature for token sampling.
27
+ stream : bool, Optional
28
+ if True, LLM generated text will be printed in terminal in real-time.
29
+ """
30
+ return NotImplemented
31
+
32
+
33
+ class LlamaCppInferenceEngine(InferenceEngine):
34
+ from llama_cpp import Llama
35
+ def __init__(self, repo_id:str, gguf_filename:str, n_ctx:int=4096, n_gpu_layers:int=-1, **kwrs):
36
+ """
37
+ The Llama.cpp inference engine.
38
+
39
+ Parameters:
40
+ ----------
41
+ repo_id : str
42
+ the exact name as shown on Huggingface repo
43
+ gguf_filename : str
44
+ the exact name as shown in Huggingface repo -> Files and versions.
45
+ If multiple gguf files are needed, use the first.
46
+ n_ctx : int, Optional
47
+ context length that LLM will evaluate.
48
+ n_gpu_layers : int, Optional
49
+ number of layers to offload to GPU. Default is all layers (-1).
50
+ """
51
+ super().__init__()
52
+ self.repo_id = repo_id
53
+ self.gguf_filename = gguf_filename
54
+ self.n_ctx = n_ctx
55
+ self.n_gpu_layers = n_gpu_layers
56
+
57
+ self.model = self.Llama.from_pretrained(
58
+ repo_id=self.repo_id,
59
+ filename=self.gguf_filename,
60
+ n_gpu_layers=n_gpu_layers,
61
+ n_ctx=n_ctx,
62
+ **kwrs
63
+ )
64
+
65
+ def __del__(self):
66
+ """
67
+ When the inference engine is deleted, release memory for model.
68
+ """
69
+ del self.model
70
+
71
+
72
+ def chat(self, messages:List[Dict[str,str]], max_new_tokens:int=2048, temperature:float=0.0, stream:bool=False, **kwrs) -> str:
73
+ """
74
+ This method inputs chat messages and outputs LLM generated text.
75
+
76
+ Parameters:
77
+ ----------
78
+ messages : List[Dict[str,str]]
79
+ a list of dict with role and content. role must be one of {"system", "user", "assistant"}
80
+ max_new_tokens : str, Optional
81
+ the max number of new tokens LLM can generate.
82
+ temperature : float, Optional
83
+ the temperature for token sampling.
84
+ stream : bool, Optional
85
+ if True, LLM generated text will be printed in terminal in real-time.
86
+ """
87
+ response = self.model.create_chat_completion(
88
+ messages=messages,
89
+ max_tokens=max_new_tokens,
90
+ temperature=temperature,
91
+ stream=stream,
92
+ **kwrs
93
+ )
94
+
95
+ if stream:
96
+ res = ''
97
+ for chunk in response:
98
+ out_dict = chunk['choices'][0]['delta']
99
+ if 'content' in out_dict:
100
+ res += out_dict['content']
101
+ print(out_dict['content'], end='', flush=True)
102
+ print('\n')
103
+ return res
104
+
105
+ return response['choices'][0]['message']['content']
106
+
107
+
108
+ class OllamaInferenceEngine(InferenceEngine):
109
+ import ollama
110
+ def __init__(self, model_name:str, num_ctx:int=4096, keep_alive:int=300, **kwrs):
111
+ """
112
+ The Ollama inference engine.
113
+
114
+ Parameters:
115
+ ----------
116
+ model_name : str
117
+ the model name exactly as shown in >> ollama ls
118
+ num_ctx : int, Optional
119
+ context length that LLM will evaluate.
120
+ keep_alive : int, Optional
121
+ seconds to hold the LLM after the last API call.
122
+ """
123
+ self.model_name = model_name
124
+ self.num_ctx = num_ctx
125
+ self.keep_alive = keep_alive
126
+
127
+ def chat(self, messages:List[Dict[str,str]], max_new_tokens:int=2048, temperature:float=0.0, stream:bool=False, **kwrs) -> str:
128
+ """
129
+ This method inputs chat messages and outputs LLM generated text.
130
+
131
+ Parameters:
132
+ ----------
133
+ messages : List[Dict[str,str]]
134
+ a list of dict with role and content. role must be one of {"system", "user", "assistant"}
135
+ max_new_tokens : str, Optional
136
+ the max number of new tokens LLM can generate.
137
+ temperature : float, Optional
138
+ the temperature for token sampling.
139
+ stream : bool, Optional
140
+ if True, LLM generated text will be printed in terminal in real-time.
141
+ """
142
+ response = self.ollama.chat(
143
+ model=self.model_name,
144
+ messages=messages,
145
+ options={'temperature':temperature, 'num_ctx': self.num_ctx, 'num_predict': max_new_tokens, **kwrs},
146
+ stream=stream,
147
+ keep_alive=self.keep_alive
148
+ )
149
+ if stream:
150
+ res = ''
151
+ for chunk in response:
152
+ res += chunk['message']['content']
153
+ print(chunk['message']['content'], end='', flush=True)
154
+ print('\n')
155
+ return res
156
+
157
+ return response['message']['content']
158
+
159
+ def release_model_memory(self):
160
+ """
161
+ Call API again with keep_alive=0 to release memory for the model
162
+ """
163
+ self.ollama.chat(model=self.model_name,
164
+ messages=[{'role': 'user', 'content': ''}],
165
+ options={'num_predict': 0},
166
+ keep_alive=0)