File size: 10,487 Bytes
e454383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import pandas as pd
import os
from utils.text_splitter import srt_chunker as chun
from utils.vector_db import VectorDB
import fitz
from datetime import datetime
import re

def load_document(file_path, raw_text=False):
    file_extension = os.path.splitext(file_path)[1].lower()
    if file_extension == '.pdf':
        return load_pdf(file_path)
    elif file_extension == '.csv':
        return load_csv(file_path)
    elif file_extension == '.srt':
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()
        paragraphs = content.split('\n\n')
        documents = [{"text": paragraph, "paragraph_number": index + 1}
                     for index, paragraph in enumerate(paragraphs) if paragraph.strip()]
        return documents
    elif file_extension == '.md':
        if raw_text:
            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()
            return content
        else:
            with open(file_path, 'r', encoding='utf-8') as file:
                content = file.read()
            paragraphs = content.split('\n\n')
            documents = [{"text": paragraph, "paragraph_number": index + 1}
                         for index, paragraph in enumerate(paragraphs) if paragraph.strip()]
            return documents
    else:
        raise ValueError(f"Unsupported file type: {file_extension}")

def load_pdf(file_path):
    """
    Load a PDF file and extract its text content into paragraphs.

    Args:
        file_path (str): The path to the PDF file.

    Returns:
        list[dict]: A list of dictionaries, each containing a paragraph of text and its paragraph number.
    """
    paragraphs = []
    pdf_document = fitz.open(file_path)

    for page in pdf_document:
        output = page.get_text("blocks")
        previous_block_id = None

        for block in output:
            if block[6] == 0:  # Check if the block is a text block
                cleaned_output = block[4].strip().replace("\n", " ")
                if cleaned_output and len(cleaned_output) >= 5:
                    if previous_block_id != block[5]:
                        paragraphs.append(cleaned_output)
                    else:
                        paragraphs[-1] += " " + cleaned_output
                    previous_block_id = block[5]

    # Adding the paragraph number to each paragraph
    documents = [{"text": paragraph, "paragraph_number": index + 1}
                 for index, paragraph in enumerate(paragraphs)]

    return documents


def load_csv(file_path):
    try:
        df = pd.read_csv(file_path)
        documents = []
        if "Paragraph text" in df.columns:
            df = df.rename(columns={"Paragraph text": "text"})
            df["page_number"] = df.index + 1
            documents = df.to_dict(orient="records")
            return documents
        else:
            for index, row in df.iterrows():
                content = row.to_json()
                documents.append({"text": content, "page_number": (index+1)})
            return documents
    except Exception as e:
        print(e)
        return []


def upload_document_to_vectorstore(chunks: list[dict], vector_db: VectorDB):
    """
    Uploads the chunks and their metadata to the vector database in batches.

    Args:
        chunks (list[dict]): A list of dictionaries, each containing a chunk of the document.
        vector_db (VectorDB): An instance of the VectorDB class to interact with the vector database.

    Returns:
        list: A list of points created in the vector store.
    """
    batch_size = 2000
    qdrant_batch_size = 500
    print(f"Uploading {len(chunks)} chunks to the vector store.")
    embeddings = []
    for batch_start in range(0, len(chunks), batch_size):
        batch_chunks = chunks[batch_start: batch_start + batch_size]
        batch_chunks = [chunk["text"] for chunk in batch_chunks]
        print(f"Encoding batch of {len(batch_chunks)} chunks.")
        batch_embeddings = vector_db.batch_encode_text(batch_chunks)

        embeddings.extend(batch_embeddings)

    points = vector_db.create_points(embeddings, chunks)
    print(f"Uploading {len(points)} points to the vector store.")

    for batch_start in range(0, len(points), qdrant_batch_size):
        batch_points = points[batch_start: batch_start + qdrant_batch_size]
        vector_db.insert_data(batch_points)
    print("Upload complete.")
    return points


def document_chunker(document: list[dict], chunking_function) -> list[dict]:
    """
    Chunk a document into smaller pieces using a provided chunking function.

    Args:
        document (list[dict]): A list of dictionaries, each containing a 'text' key with the document content.
        chunking_function (callable): A function that takes a string and returns a list of chunks.

    Returns:
        list[dict]: A list of dictionaries, each containing a chunk of the document.
    """
    chunks = []
    for page in document:
        text = page.pop("text")
        text_chunks = chunking_function(text)
        for chunk in text_chunks:
            chunks.append({"text": chunk, **page})

    return chunks


def transcript_chunker(document: list[dict], filename) -> list[dict]:
    """
    Chunk a transcript document into smaller pieces.

    Args:
        document (list[dict]): A list of dictionaries, each containing a 'text' key with the transcript content.

    Returns:
        list[dict]: A list of dictionaries, each containing a chunk of the transcript.
    """

    chunks = []

    def transcript_formatter(
        transcript): return f"{transcript['timestamp']} {transcript['speaker']}: {transcript['text']}"
    count = 0
    for page in document:
        text = page.pop("text")
        transcript = chun(text)

        if transcript is None:
            if "File name" in text:
                continue
            if chunks:
                chunks[-1]["text"] += text
            continue

        # Add the filename to the transcript
        transcript["filename"] = filename

        # Adding the index of the transcript to the transcript
        transcript["index"] = count
        count += 1

        # Add the current transcript to the chunks
        chunks.append(transcript)

        # Add the previous message to the current message
        if len(chunks) > 1:
            chunks[-1]["previous_message"] = transcript_formatter(chunks[-2])

        # Add the next message to the previous message
        if len(chunks) > 1:
            chunks[-2]["next_message"] = transcript_formatter(chunks[-1])

    return chunks

def merge_chunks(chunks: list[dict]) -> list[dict]:
    """
    Merge consecutive chunks that have the same speaker

    Args:
        chunks (list[dict]): A list of dictionaries, each containing a chunk of text and its metadata.

    Returns:
        list[dict]: A list of merged chunks.
    """
    if not chunks:
        return []

    merged_chunks = []
    current_chunk = chunks[0]
    for chunk in chunks[1:]:
        if chunk["speaker"] == current_chunk["speaker"]:
            # Merge the text of the current chunk with the next one
            current_chunk["text"] += " " + chunk["text"]
        else:
            # Add the current chunk to the merged list and start a new one
            merged_chunks.append(current_chunk)
            current_chunk = chunk
    # Add the last chunk
    merged_chunks.append(current_chunk)
    return merged_chunks


def generate_filename_from_details(minute_block_md: str) -> str:
    """
    Generates a filename from a minute block by extracting details 
    from a line like "| 24 March 2025, Guoqi Wu, AVP CSD | |".
    Format: YYYY-MM-DD_Name1Name2_RolePart1_RolePart2_CLE.md
    """
    lines = minute_block_md.splitlines()
    details_str = None

    if len(lines) > 1:
        # The line containing details for the filename is typically the second line of the block
        # e.g., | 24 March 2025, Guoqi Wu, AVP CSD | |
        header_info_line = lines[1] 
        
        # Extract content from between the first and second pipe characters
        line_parts = header_info_line.split('|')
        if len(line_parts) > 2 and line_parts[1].strip():
            details_str = line_parts[1].strip()
        else:
            raise ValueError(f"Could not parse details for filename from line: '{header_info_line}' in minute block.")
    else:
        raise ValueError("Minute block has insufficient lines to extract details for filename.")

    if not details_str:
        raise ValueError("Failed to extract details string from minute block.")

    # Regex to separate date from the rest of the details
    match = re.match(r"(\d{1,2}\s+[A-Za-z]+\s+\d{4}),\s*(.+)", details_str)
    if not match:
        raise ValueError(f"Details string '{details_str}' does not match expected format 'Date, Details...'")

    date_part_str = match.group(1) 
    name_and_role_parts_str = match.group(2)

    # Convert date to YYYY-MM-DD
    try:
        dt_object = datetime.strptime(date_part_str, "%d %B %Y")
        formatted_date = dt_object.strftime("%Y-%m-%d")
    except ValueError:
        try:
            dt_object = datetime.strptime(date_part_str, "%d %b %Y")
            formatted_date = dt_object.strftime("%Y-%m-%d")
        except ValueError as e:
            raise ValueError(f"Date string '{date_part_str}' could not be parsed: {e}")

    parts = [part.replace(" ", "") for part in name_and_role_parts_str.split(", ")]
    combined_name_role = "_".join(parts)
    combined_name_role_sanitized = re.sub(r'[^\w-]', '', combined_name_role.replace("(", "").replace(")", ""))

    return f"{formatted_date}_{combined_name_role_sanitized}_CLE.md", (formatted_date, combined_name_role_sanitized)

def load_prompt(prompt_file_name:str, working_directory) -> str:
    """
    Load a prompt from a file in the specified working directory's prompts folder.

    Args:
        prompt_file_name (str): The name of the prompt file to load.
        working_directory (str): The directory where the prompt file is located.

    Returns:
        str: The content of the prompt file.
    """
    prompt_file_path = os.path.join(working_directory, "prompts", prompt_file_name+".txt")
    if not os.path.exists(prompt_file_path):
        raise FileNotFoundError(f"Prompt file '{prompt_file_name}' at path '{prompt_file_path}' does not exist.")

    with open(prompt_file_path, 'r', encoding='utf-8') as file:
        return file.read()