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 +0 -0
- llm_ie/data_types.py +167 -0
- llm_ie/engines.py +166 -0
- llm_ie/extractors.py +496 -0
- llm_ie/prompt_editor.py +26 -0
- llm_ie-0.1.0.dist-info/METADATA +552 -0
- llm_ie-0.1.0.dist-info/RECORD +8 -0
- llm_ie-0.1.0.dist-info/WHEEL +4 -0
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)
|