WIP with Codex
This commit is contained in:
@@ -123,5 +123,6 @@ class AsrEventizer:
|
|||||||
text=preevent.text,
|
text=preevent.text,
|
||||||
payload=None
|
payload=None
|
||||||
))
|
))
|
||||||
|
output_id += 1
|
||||||
# final timeline
|
# final timeline
|
||||||
return Timeline(events=events)
|
return Timeline(events=events)
|
||||||
59
main.py
59
main.py
@@ -14,10 +14,11 @@ import torch
|
|||||||
from asr import Asr, AsrRawResult
|
from asr import Asr, AsrRawResult
|
||||||
from asr_filter import AsrFilter, AsrFilterResult
|
from asr_filter import AsrFilter, AsrFilterResult
|
||||||
from asr_eventizer import AsrEventizer
|
from asr_eventizer import AsrEventizer
|
||||||
|
from structure_builder import StructureBuilder
|
||||||
from windowizer import Windowizer
|
from windowizer import Windowizer
|
||||||
|
|
||||||
from agent import Agent
|
from agent import Agent
|
||||||
from utils import ffmpeg_split_video, ffmpeg_to_mp3
|
from utils import Timeline, ffmpeg_split_video, ffmpeg_to_mp3
|
||||||
|
|
||||||
ARGS: argparse.Namespace
|
ARGS: argparse.Namespace
|
||||||
|
|
||||||
@@ -170,6 +171,54 @@ def on_asr_events(current_step: Step, input_data: dict | None) -> tuple[Step | N
|
|||||||
result = eventizer.eventize(AsrFilterResult(**input_data))
|
result = eventizer.eventize(AsrFilterResult(**input_data))
|
||||||
return (Step.VIDEO_REFERENCES, result.model_dump(mode="json"))
|
return (Step.VIDEO_REFERENCES, result.model_dump(mode="json"))
|
||||||
|
|
||||||
|
def on_video_references(current_step: Step, input_data: dict | None) -> tuple[Step | None, dict | None]:
|
||||||
|
# do not filter if output file exists
|
||||||
|
if os.path.isfile(WORKFLOW_DATA[current_step][0]):
|
||||||
|
logging.info("Skipping video references")
|
||||||
|
with open(WORKFLOW_DATA[current_step][0], "rb") as f:
|
||||||
|
return (Step.REFERENCE_RESOLVER, json.load(f))
|
||||||
|
if not os.path.isfile("video.mp4"):
|
||||||
|
logging.info("No video, skipping")
|
||||||
|
return (Step.REFERENCE_RESOLVER, None)
|
||||||
|
# NOT IMPLEMENTED
|
||||||
|
logging.warning("Video references are not implemented yet")
|
||||||
|
return (Step.REFERENCE_RESOLVER, None)
|
||||||
|
|
||||||
|
def on_reference_resolver(current_step: Step, input_data: dict | None) -> tuple[Step | None, dict | None]:
|
||||||
|
# do not filter if output file exists
|
||||||
|
if os.path.isfile(WORKFLOW_DATA[current_step][0]):
|
||||||
|
logging.info("Skipping reference resolver")
|
||||||
|
with open(WORKFLOW_DATA[current_step][0], "rb") as f:
|
||||||
|
return (Step.STRUCTURE_BUILDER, json.load(f))
|
||||||
|
if not os.path.isfile("video.mp4"):
|
||||||
|
logging.info("No video, skipping")
|
||||||
|
return (Step.STRUCTURE_BUILDER, input_data)
|
||||||
|
# NOT IMPLEMENTED
|
||||||
|
logging.warning("Reference resolver is not implemented yet")
|
||||||
|
return (Step.STRUCTURE_BUILDER, input_data)
|
||||||
|
|
||||||
|
def on_structure_builder(current_step: Step, input_data: dict | None) -> tuple[Step | None, dict | None]:
|
||||||
|
# don't if done
|
||||||
|
if os.path.isfile(WORKFLOW_DATA[current_step][0]):
|
||||||
|
logging.info("Skipping structure builder")
|
||||||
|
with open(WORKFLOW_DATA[current_step][0], "rb") as f:
|
||||||
|
return (Step.MARKDOWN_BUILDER, json.load(f))
|
||||||
|
if input_data is None:
|
||||||
|
logging.error("Can't build document structure without input_data")
|
||||||
|
return (None, None)
|
||||||
|
logging.info("Creating the agent")
|
||||||
|
agent = Agent(
|
||||||
|
model=ARGS.ai_model,
|
||||||
|
base_url=ARGS.ai_base_url,
|
||||||
|
api_key=ARGS.ai_api_key
|
||||||
|
)
|
||||||
|
logging.info("Creating the windowizer")
|
||||||
|
windowizer = Windowizer()
|
||||||
|
logging.info("Building document structure...")
|
||||||
|
builder = StructureBuilder(agent, windowizer)
|
||||||
|
result = builder.build(Timeline(**input_data))
|
||||||
|
return (Step.MARKDOWN_BUILDER, result.model_dump(mode="json"))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Main
|
# Main
|
||||||
#
|
#
|
||||||
@@ -178,9 +227,9 @@ WORKFLOW_DATA: dict[Step, tuple[str, Callable[[Step, dict | None], tuple[Step |
|
|||||||
Step.VOICE_RECOGNITION: ("asr_raw.json", on_voice_recognition),
|
Step.VOICE_RECOGNITION: ("asr_raw.json", on_voice_recognition),
|
||||||
Step.ASR_FILTER: ("asr.json", on_asr_filter),
|
Step.ASR_FILTER: ("asr.json", on_asr_filter),
|
||||||
Step.ASR_EVENTS: ("audio_events.json", on_asr_events),
|
Step.ASR_EVENTS: ("audio_events.json", on_asr_events),
|
||||||
Step.VIDEO_REFERENCES: ("unresolved.json", None),
|
Step.VIDEO_REFERENCES: ("unresolved.json", on_video_references),
|
||||||
Step.REFERENCE_RESOLVER: ("events.json", None),
|
Step.REFERENCE_RESOLVER: ("events.json", on_reference_resolver),
|
||||||
Step.STRUCTURE_BUILDER: ("structure.json", None),
|
Step.STRUCTURE_BUILDER: ("structure.json", on_structure_builder),
|
||||||
Step.MARKDOWN_BUILDER: ("output.md", None)
|
Step.MARKDOWN_BUILDER: ("output.md", None)
|
||||||
}
|
}
|
||||||
"""Information about workflow.
|
"""Information about workflow.
|
||||||
@@ -224,4 +273,4 @@ if __name__ == "__main__":
|
|||||||
except SystemExit:
|
except SystemExit:
|
||||||
raise
|
raise
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|||||||
588
prompts/structure_builder.md
Normal file
588
prompts/structure_builder.md
Normal file
@@ -0,0 +1,588 @@
|
|||||||
|
Ты преобразуешь временную шкалу лекции в структурированные элементы будущего Markdown-документа.
|
||||||
|
|
||||||
|
Ты НЕ пишешь Markdown напрямую.
|
||||||
|
Ты создаёшь только JSON-структуру документа.
|
||||||
|
|
||||||
|
Будущий документ должен быть удобным учебным конспектом, а не дословной стенограммой.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ФОРМАТ ВХОДА
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
На вход поступает JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"past": [...],
|
||||||
|
"present": [...],
|
||||||
|
"future": [...],
|
||||||
|
"context": {...}
|
||||||
|
}
|
||||||
|
|
||||||
|
Все события расположены в хронологическом порядке.
|
||||||
|
|
||||||
|
Событие может иметь один из трёх типов.
|
||||||
|
|
||||||
|
1. Голосовое событие:
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "voice",
|
||||||
|
"timestamp": 120.0,
|
||||||
|
"duration": 8.0,
|
||||||
|
"text": "Осмысленная речь преподавателя.",
|
||||||
|
"payload": null
|
||||||
|
}
|
||||||
|
|
||||||
|
`text` содержит речь преподавателя.
|
||||||
|
|
||||||
|
2. Визуальное событие:
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"type": "vis",
|
||||||
|
"timestamp": 128.0,
|
||||||
|
"duration": 0.0,
|
||||||
|
"text": "Описание содержимого выбранного кадра.",
|
||||||
|
"payload": "images/123.jpg"
|
||||||
|
}
|
||||||
|
|
||||||
|
`text` описывает содержание изображения.
|
||||||
|
`payload` содержит путь к изображению.
|
||||||
|
|
||||||
|
3. OCR-событие:
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"type": "ocr",
|
||||||
|
"timestamp": 132.0,
|
||||||
|
"duration": 0.0,
|
||||||
|
"text": "Описание того, какой текст был найден на экране.",
|
||||||
|
"payload": "Текст, распознанный непосредственно с изображения."
|
||||||
|
}
|
||||||
|
|
||||||
|
Для `ocr` наиболее важным содержанием является `payload`.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ОСНОВНАЯ ЗАДАЧА
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Преобразуй события из `present` в элементы учебного документа.
|
||||||
|
|
||||||
|
Ты должен:
|
||||||
|
|
||||||
|
- объединять связанные события в законченные мысли;
|
||||||
|
- устранять особенности устной речи;
|
||||||
|
- удалять бессодержательные повторы;
|
||||||
|
- удалять слова-паразиты и организационный шум;
|
||||||
|
- объединять повторные формулировки одной мысли;
|
||||||
|
- создавать логические разделы и подразделы;
|
||||||
|
- превращать перечисления в списки;
|
||||||
|
- выделять определения;
|
||||||
|
- выделять действительно важные утверждения;
|
||||||
|
- включать полезную информацию из OCR;
|
||||||
|
- добавлять изображения, когда они действительно помогают понять материал;
|
||||||
|
- сохранять важные примеры и пояснения преподавателя.
|
||||||
|
|
||||||
|
Ты можешь существенно перерабатывать ФОРМУ речи.
|
||||||
|
|
||||||
|
Ты НЕ должен изменять её СМЫСЛ.
|
||||||
|
|
||||||
|
Ты НЕ должен придумывать факты, которых нет во входных событиях.
|
||||||
|
|
||||||
|
Ты НЕ обязан использовать каждое событие.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
PAST
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
`past` содержит события непосредственно перед текущим окном.
|
||||||
|
|
||||||
|
Они нужны только для понимания контекста.
|
||||||
|
|
||||||
|
Не создавай элементы документа повторно на основе уже обработанного `past`.
|
||||||
|
|
||||||
|
Использовать содержание из `past` в новом элементе разрешается только если:
|
||||||
|
|
||||||
|
- во входном `context.pending` указано, что эта мысль осталась незавершённой;
|
||||||
|
- и события из текущего `present` действительно завершают её.
|
||||||
|
|
||||||
|
Во всех остальных случаях `past` является read-only контекстом.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
PRESENT
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
`present` — основная часть текущего окна.
|
||||||
|
|
||||||
|
Именно события из `present` необходимо преобразовать в элементы документа.
|
||||||
|
|
||||||
|
Одно событие может:
|
||||||
|
|
||||||
|
- породить один элемент;
|
||||||
|
- использоваться совместно с соседними событиями;
|
||||||
|
- не попасть в документ, если оно не несёт полезной информации.
|
||||||
|
|
||||||
|
Не пытайся создать отдельный элемент для каждого события.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
FUTURE
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
`future` предоставлен ТОЛЬКО для понимания того, продолжается ли мысль дальше.
|
||||||
|
|
||||||
|
КРИТИЧЕСКОЕ ПРАВИЛО:
|
||||||
|
|
||||||
|
НЕ создавай элементы документа на основе содержимого `future`.
|
||||||
|
|
||||||
|
НЕ используй `future` как источник фактов для текущих элементов.
|
||||||
|
|
||||||
|
НЕ создавай `image` для события из `future`.
|
||||||
|
|
||||||
|
Ты можешь только прочитать `future`, чтобы понять, закончена ли мысль внутри `present`.
|
||||||
|
|
||||||
|
Если мысль начинается в `present`, но явно продолжается в `future`, НЕ создавай незаконченный элемент.
|
||||||
|
|
||||||
|
Вместо этого сохрани её в `context.pending`.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
СТИЛЬ БУДУЩЕГО ДОКУМЕНТА
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Документ должен быть похож на качественный конспект лекции.
|
||||||
|
|
||||||
|
Предпочитай:
|
||||||
|
|
||||||
|
- ясные законченные предложения;
|
||||||
|
- компактные абзацы;
|
||||||
|
- логичную иерархию;
|
||||||
|
- списки вместо длинных перечислений;
|
||||||
|
- сохранение терминологии преподавателя;
|
||||||
|
- сохранение содержательных примеров.
|
||||||
|
|
||||||
|
Удаляй:
|
||||||
|
|
||||||
|
- слова-паразиты;
|
||||||
|
- бессодержательные повторы;
|
||||||
|
- повторное чтение одного и того же текста;
|
||||||
|
- технические проблемы конференции;
|
||||||
|
- перекличку;
|
||||||
|
- обсуждение присутствия студентов;
|
||||||
|
- случайные реплики;
|
||||||
|
- фразы, не несущие ценности для конспекта.
|
||||||
|
|
||||||
|
Не удаляй существенную информацию только потому, что она сформулирована разговорно.
|
||||||
|
|
||||||
|
Если одна мысль была произнесена несколько раз, обычно оставь одну наиболее полную и ясную формулировку.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ДОПУСТИМЫЕ ЭЛЕМЕНТЫ
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Разрешены ТОЛЬКО следующие типы.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
1. heading
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"level": 2,
|
||||||
|
"text": "Основы системного анализа"
|
||||||
|
}
|
||||||
|
|
||||||
|
Допустимые уровни:
|
||||||
|
|
||||||
|
2 — крупный раздел;
|
||||||
|
3 — подраздел;
|
||||||
|
4 — небольшой локальный подраздел.
|
||||||
|
|
||||||
|
Не используй level 1.
|
||||||
|
|
||||||
|
Не создавай заголовок для каждого небольшого абзаца.
|
||||||
|
|
||||||
|
Заголовок должен отражать реально обсуждаемую тему, а не придуманную тобой новую классификацию.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
2. paragraph
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"text": "Системный анализ используется для исследования сложных систем в условиях неопределённости."
|
||||||
|
}
|
||||||
|
|
||||||
|
Один `paragraph` должен содержать одну законченную мысль или небольшой связный набор мыслей.
|
||||||
|
|
||||||
|
Не создавай огромные параграфы.
|
||||||
|
|
||||||
|
Если материал естественно представляет перечисление — используй список.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
3. unordered
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "unordered",
|
||||||
|
"items": [
|
||||||
|
"Повышение качества.",
|
||||||
|
"Снижение себестоимости.",
|
||||||
|
"Повышение надёжности."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Используй, когда несколько пунктов однородны, но их порядок несущественен.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
4. ordered
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "ordered",
|
||||||
|
"items": [
|
||||||
|
"Определить цель исследования.",
|
||||||
|
"Построить модель объекта.",
|
||||||
|
"Рассмотреть альтернативные решения."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Используй, когда:
|
||||||
|
|
||||||
|
- порядок элементов важен;
|
||||||
|
- преподаватель явно использует нумерацию;
|
||||||
|
- перечисляются этапы;
|
||||||
|
- перечисляются принципы;
|
||||||
|
- перечисляются последовательные действия.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
5. definition
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "definition",
|
||||||
|
"term": "Системный анализ",
|
||||||
|
"text": "Совокупность методологических средств..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Используй только для настоящих определений терминов или понятий.
|
||||||
|
|
||||||
|
Не превращай обычное объяснение в `definition`.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
6. important
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "important",
|
||||||
|
"text": "Цели частей системы не должны противоречить общей цели системы."
|
||||||
|
}
|
||||||
|
|
||||||
|
Используй только для действительно важных утверждений:
|
||||||
|
|
||||||
|
- преподаватель явно делает акцент;
|
||||||
|
- правило принципиально важно;
|
||||||
|
- материал явно требуется запомнить;
|
||||||
|
- говорится о важном требовании или ограничении.
|
||||||
|
|
||||||
|
Не злоупотребляй `important`.
|
||||||
|
|
||||||
|
--------------------------------------------------
|
||||||
|
7. image
|
||||||
|
--------------------------------------------------
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"event_id": 54
|
||||||
|
}
|
||||||
|
|
||||||
|
Используй ТОЛЬКО для события с `type = "vis"`.
|
||||||
|
|
||||||
|
`event_id` должен быть реальным ID соответствующего `vis`-события.
|
||||||
|
|
||||||
|
Добавляй изображение только если оно действительно полезно для понимания материала.
|
||||||
|
|
||||||
|
Не добавляй изображение только потому, что `vis`-событие существует.
|
||||||
|
|
||||||
|
Если изображение:
|
||||||
|
- является схемой;
|
||||||
|
- графиком;
|
||||||
|
- диаграммой;
|
||||||
|
- рисунком;
|
||||||
|
- визуально важным слайдом;
|
||||||
|
- объектом, который трудно полноценно выразить текстом;
|
||||||
|
|
||||||
|
то обычно его следует включить.
|
||||||
|
|
||||||
|
Не описывай путь к изображению самостоятельно.
|
||||||
|
Renderer получит его позже по `event_id`.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
VOICE
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Для `voice` основным источником информации является поле `text`.
|
||||||
|
|
||||||
|
Речь разрешается перерабатывать в нормальный письменный текст.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
"Ну вот смотрите мы с вами получается сейчас рассмотрели три задачи анализа"
|
||||||
|
|
||||||
|
может стать:
|
||||||
|
|
||||||
|
"Были рассмотрены три основные задачи анализа."
|
||||||
|
|
||||||
|
Но нельзя добавлять новые сведения или менять смысл.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
OCR
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Для `ocr` используй прежде всего `payload`.
|
||||||
|
|
||||||
|
OCR-текст может быть:
|
||||||
|
|
||||||
|
- определением;
|
||||||
|
- формулой;
|
||||||
|
- списком;
|
||||||
|
- заголовком;
|
||||||
|
- таблицей;
|
||||||
|
- обычным текстом.
|
||||||
|
|
||||||
|
Преобразуй его в подходящие элементы документа.
|
||||||
|
|
||||||
|
Если преподаватель читает вслух тот же текст, который содержится в OCR, НЕ дублируй информацию.
|
||||||
|
|
||||||
|
Объедини оба источника в одну нормальную формулировку.
|
||||||
|
|
||||||
|
Если OCR и речь немного различаются, не пытайся самостоятельно угадывать, какой вариант фактически правильный, если это нельзя определить из контекста.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
VIS
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
`vis` содержит уже найденное и проверенное изображение.
|
||||||
|
|
||||||
|
Поле `text` помогает понять, что находится на изображении.
|
||||||
|
|
||||||
|
Не копируй описание изображения как отдельный paragraph без необходимости.
|
||||||
|
|
||||||
|
Обычно `vis` используется для принятия решения:
|
||||||
|
|
||||||
|
нужно ли добавить
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"event_id": ...
|
||||||
|
}
|
||||||
|
|
||||||
|
в структуру документа.
|
||||||
|
|
||||||
|
Размещай `image` рядом с текстом, к которому изображение относится.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
СОВМЕСТНОЕ ИСПОЛЬЗОВАНИЕ ИСТОЧНИКОВ
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
События разных типов могут описывать одну и ту же часть лекции.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
voice:
|
||||||
|
"Посмотрите на график зависимости температуры..."
|
||||||
|
|
||||||
|
vis:
|
||||||
|
"График зависимости температуры от времени."
|
||||||
|
|
||||||
|
ocr:
|
||||||
|
"Температура, °C / Время, с"
|
||||||
|
|
||||||
|
Не создавай три независимых повторяющихся фрагмента.
|
||||||
|
|
||||||
|
Вместо этого может получиться:
|
||||||
|
|
||||||
|
- paragraph с объяснением;
|
||||||
|
- image с соответствующим `event_id`.
|
||||||
|
|
||||||
|
Используй разные источники совместно для построения одной логической части документа.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ГРАНИЦЫ ОКНА
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Элемент можно создавать только если его смысл завершён внутри `present`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
present:
|
||||||
|
"Первый принцип системного анализа заключается в..."
|
||||||
|
|
||||||
|
future:
|
||||||
|
"...необходимости чётко определить цель."
|
||||||
|
|
||||||
|
НЕ создавай:
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"text": "Первый принцип системного анализа заключается в..."
|
||||||
|
}
|
||||||
|
|
||||||
|
Сохрани незавершённую мысль в `context.pending`.
|
||||||
|
|
||||||
|
На следующей итерации она попадёт в `past`, а продолжение — в `present`.
|
||||||
|
|
||||||
|
Тогда можно создать законченный элемент.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
CONTEXT
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
`context` — очень маленькая рабочая память между окнами.
|
||||||
|
|
||||||
|
Используй строго такую структуру:
|
||||||
|
|
||||||
|
{
|
||||||
|
"current_section": null,
|
||||||
|
"current_subsection": null,
|
||||||
|
"recent_headings": [],
|
||||||
|
"pending": null
|
||||||
|
}
|
||||||
|
|
||||||
|
`current_section`:
|
||||||
|
- текущий заголовок уровня 2;
|
||||||
|
- строка или null;
|
||||||
|
- максимум 120 символов.
|
||||||
|
|
||||||
|
`current_subsection`:
|
||||||
|
- текущий заголовок уровня 3 или 4;
|
||||||
|
- строка или null;
|
||||||
|
- максимум 120 символов.
|
||||||
|
|
||||||
|
`recent_headings`:
|
||||||
|
- последние заголовки документа;
|
||||||
|
- максимум 6 строк;
|
||||||
|
- используется только чтобы не создавать повторяющиеся или нелогичные заголовки.
|
||||||
|
|
||||||
|
`pending`:
|
||||||
|
- незаконченная мысль на границе окна;
|
||||||
|
- null, если такой мысли нет.
|
||||||
|
|
||||||
|
Формат `pending`:
|
||||||
|
|
||||||
|
{
|
||||||
|
"event_ids": [40, 41],
|
||||||
|
"summary": "Началось объяснение второго принципа системного анализа, продолжение находится в следующем окне."
|
||||||
|
}
|
||||||
|
|
||||||
|
`pending.event_ids`:
|
||||||
|
- должны относиться к событиям из текущего `present`;
|
||||||
|
- эти события не должны быть полностью оформлены в готовые элементы.
|
||||||
|
|
||||||
|
`pending.summary`:
|
||||||
|
- максимум 180 символов;
|
||||||
|
- краткая подсказка для следующей итерации;
|
||||||
|
- не является частью итогового документа.
|
||||||
|
|
||||||
|
На следующей итерации `pending` используется только как подсказка.
|
||||||
|
|
||||||
|
Фактические события имеют больший приоритет.
|
||||||
|
|
||||||
|
Не превращай `context` в конспект.
|
||||||
|
Не сохраняй там подробное содержание лекции.
|
||||||
|
Не копируй туда длинные фрагменты текста.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ЗАГОЛОВКИ МЕЖДУ ОКНАМИ
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Перед созданием нового заголовка учитывай:
|
||||||
|
|
||||||
|
- `current_section`;
|
||||||
|
- `current_subsection`;
|
||||||
|
- `recent_headings`;
|
||||||
|
- содержимое `past`.
|
||||||
|
|
||||||
|
Не повторяй существующий заголовок только потому, что новая итерация началась посередине того же раздела.
|
||||||
|
|
||||||
|
Если текущая тема продолжается — продолжай создавать содержательные элементы без нового heading.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ПОРЯДОК ЭЛЕМЕНТОВ
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Все элементы должны идти в том порядке, в котором читатель должен увидеть их в документе.
|
||||||
|
|
||||||
|
Обычно:
|
||||||
|
|
||||||
|
heading
|
||||||
|
paragraph
|
||||||
|
image
|
||||||
|
paragraph
|
||||||
|
list
|
||||||
|
|
||||||
|
или другая логичная последовательность.
|
||||||
|
|
||||||
|
Хронология лекции является основой порядка, но внутри одной небольшой смысловой части разрешается разместить изображение непосредственно рядом с относящимся к нему текстом.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
OUTPUT
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Верни РОВНО один валидный JSON:
|
||||||
|
|
||||||
|
{
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "heading",
|
||||||
|
"level": 2,
|
||||||
|
"text": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "paragraph",
|
||||||
|
"text": "..."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"context": {
|
||||||
|
"current_section": null,
|
||||||
|
"current_subsection": null,
|
||||||
|
"recent_headings": [],
|
||||||
|
"pending": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Никакого Markdown.
|
||||||
|
Никаких ```json.
|
||||||
|
Никакого текста перед JSON.
|
||||||
|
Никакого текста после JSON.
|
||||||
|
Никаких комментариев.
|
||||||
|
Никаких неизвестных типов элементов.
|
||||||
|
|
||||||
|
Если в текущем `present` нет материала, достойного добавления в итоговый документ:
|
||||||
|
|
||||||
|
{
|
||||||
|
"elements": [],
|
||||||
|
"context": {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
— это корректный результат.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ПРОВЕРКА ПЕРЕД ОТВЕТОМ
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Перед ответом проверь:
|
||||||
|
|
||||||
|
1. Все элементы используют только разрешённые типы.
|
||||||
|
2. Каждый `image.event_id` существует во входе.
|
||||||
|
3. Каждый `image.event_id` относится к событию типа `vis`.
|
||||||
|
4. Ни один image не создан для события из `future`.
|
||||||
|
5. Содержимое `future` не было использовано как материал готового элемента.
|
||||||
|
6. Содержательная мысль не оборвана на границе окна.
|
||||||
|
7. Незаконченная мысль сохранена в `context.pending`.
|
||||||
|
8. Старый `pending`, если он завершился, обработан с учётом реальных событий.
|
||||||
|
9. Одинаковая информация из voice/OCR/vis не продублирована без причины.
|
||||||
|
10. Заголовки не создаются заново только из-за начала нового окна.
|
||||||
|
11. Ты не добавил фактов, отсутствующих во входных событиях.
|
||||||
|
12. `context` остаётся коротким.
|
||||||
|
13. Ответ является валидным JSON.
|
||||||
236
structure_builder.py
Normal file
236
structure_builder.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from typing import Annotated, Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
||||||
|
|
||||||
|
from agent import Agent, AgentMessage
|
||||||
|
from utils import Event, Timeline
|
||||||
|
from windowizer import Window, Windowizer
|
||||||
|
|
||||||
|
|
||||||
|
class _StrictModel(BaseModel):
|
||||||
|
"""Base model for schemas returned by the agent."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid", strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
class HeadingElement(_StrictModel):
|
||||||
|
"""A document section heading."""
|
||||||
|
|
||||||
|
type: Literal["heading"]
|
||||||
|
level: Literal[2, 3, 4]
|
||||||
|
text: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ParagraphElement(_StrictModel):
|
||||||
|
"""A paragraph containing one complete thought."""
|
||||||
|
|
||||||
|
type: Literal["paragraph"]
|
||||||
|
text: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class UnorderedListElement(_StrictModel):
|
||||||
|
"""A list whose item order is not significant."""
|
||||||
|
|
||||||
|
type: Literal["unordered"]
|
||||||
|
items: list[str] = Field(min_length=1)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_items(self) -> "UnorderedListElement":
|
||||||
|
if any(not item.strip() for item in self.items):
|
||||||
|
raise ValueError("List items must not be empty")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedListElement(_StrictModel):
|
||||||
|
"""A list whose item order is significant."""
|
||||||
|
|
||||||
|
type: Literal["ordered"]
|
||||||
|
items: list[str] = Field(min_length=1)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_items(self) -> "OrderedListElement":
|
||||||
|
if any(not item.strip() for item in self.items):
|
||||||
|
raise ValueError("List items must not be empty")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class DefinitionElement(_StrictModel):
|
||||||
|
"""A definition of a term or concept."""
|
||||||
|
|
||||||
|
type: Literal["definition"]
|
||||||
|
term: str = Field(min_length=1)
|
||||||
|
text: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ImportantElement(_StrictModel):
|
||||||
|
"""An important statement that should stand out in the document."""
|
||||||
|
|
||||||
|
type: Literal["important"]
|
||||||
|
text: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageElement(_StrictModel):
|
||||||
|
"""A reference to an image stored in a visual timeline event."""
|
||||||
|
|
||||||
|
type: Literal["image"]
|
||||||
|
event_id: int
|
||||||
|
|
||||||
|
|
||||||
|
StructureElement = Annotated[
|
||||||
|
HeadingElement
|
||||||
|
| ParagraphElement
|
||||||
|
| UnorderedListElement
|
||||||
|
| OrderedListElement
|
||||||
|
| DefinitionElement
|
||||||
|
| ImportantElement
|
||||||
|
| ImageElement,
|
||||||
|
Field(discriminator="type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Structure(_StrictModel):
|
||||||
|
"""Structure of the future Markdown document."""
|
||||||
|
|
||||||
|
elements: list[StructureElement]
|
||||||
|
|
||||||
|
|
||||||
|
class _Pending(_StrictModel):
|
||||||
|
"""An unfinished thought carried over to the next window."""
|
||||||
|
|
||||||
|
event_ids: list[int] = Field(min_length=1)
|
||||||
|
summary: str = Field(min_length=1, max_length=180)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_event_ids(self) -> "_Pending":
|
||||||
|
if len(self.event_ids) != len(set(self.event_ids)):
|
||||||
|
raise ValueError("pending.event_ids must not contain duplicates")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class _BuilderContext(_StrictModel):
|
||||||
|
"""Small amount of document state preserved between agent calls."""
|
||||||
|
|
||||||
|
current_section: str | None = Field(default=None, max_length=120)
|
||||||
|
current_subsection: str | None = Field(default=None, max_length=120)
|
||||||
|
recent_headings: list[str] = Field(default_factory=list, max_length=6)
|
||||||
|
pending: _Pending | None = None
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_recent_headings(self) -> "_BuilderContext":
|
||||||
|
if any(not heading.strip() or len(heading) > 120 for heading in self.recent_headings):
|
||||||
|
raise ValueError("recent_headings must contain non-empty strings up to 120 characters")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class _BuildResult(_StrictModel):
|
||||||
|
"""Result returned by the agent for a single window."""
|
||||||
|
|
||||||
|
elements: list[StructureElement]
|
||||||
|
context: _BuilderContext
|
||||||
|
|
||||||
|
|
||||||
|
class StructureBuilder:
|
||||||
|
"""Build a document structure from a timeline of lecture events."""
|
||||||
|
|
||||||
|
DEBUG_ID = 0
|
||||||
|
MAX_RETRIES = 5
|
||||||
|
|
||||||
|
def __init__(self, agent: Agent, windowizer: Windowizer[Event]) -> None:
|
||||||
|
"""Create a structure builder.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent: Agent used to transform timeline windows.
|
||||||
|
windowizer: Windowizer used to split timeline events.
|
||||||
|
"""
|
||||||
|
self._agent = agent
|
||||||
|
self._windowizer = windowizer
|
||||||
|
with open("prompts/structure_builder.md", "r", encoding="utf-8") as f:
|
||||||
|
self._system_prompt = AgentMessage(content=f.read(), role="system")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_result(result: _BuildResult, window: Window[Event]) -> None:
|
||||||
|
"""Validate constraints that depend on the current input window."""
|
||||||
|
usable_visual_ids = {
|
||||||
|
event.id
|
||||||
|
for event in window.past + window.present
|
||||||
|
if event.type == "vis"
|
||||||
|
}
|
||||||
|
for element in result.elements:
|
||||||
|
if isinstance(element, ImageElement) and element.event_id not in usable_visual_ids:
|
||||||
|
raise ValueError(
|
||||||
|
"image.event_id must reference a vis event from past or present"
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.context.pending is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
window_ids = {
|
||||||
|
event.id
|
||||||
|
for event in window.past + window.present + window.future
|
||||||
|
}
|
||||||
|
invalid_pending_ids = (
|
||||||
|
set(result.context.pending.event_ids) - window_ids
|
||||||
|
)
|
||||||
|
if invalid_pending_ids:
|
||||||
|
raise ValueError(
|
||||||
|
"pending.event_ids must reference events from the current window"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_window(self, window: Window[Event]) -> _BuildResult:
|
||||||
|
"""Build document elements from a single timeline window."""
|
||||||
|
messages = [
|
||||||
|
self._system_prompt,
|
||||||
|
AgentMessage(
|
||||||
|
content=json.dumps(
|
||||||
|
window.model_dump(mode="json"),
|
||||||
|
indent=2,
|
||||||
|
ensure_ascii=False,
|
||||||
|
),
|
||||||
|
role="user",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
debug_dir = None
|
||||||
|
if os.path.isdir("debug"):
|
||||||
|
debug_dir = f"debug/StructureBuilder/{StructureBuilder.DEBUG_ID}"
|
||||||
|
StructureBuilder.DEBUG_ID += 1
|
||||||
|
os.makedirs(debug_dir, exist_ok=True)
|
||||||
|
with open(f"{debug_dir}/request.txt", "w", encoding="utf-8") as f:
|
||||||
|
f.write(messages[1].content)
|
||||||
|
|
||||||
|
retries_left = self.MAX_RETRIES
|
||||||
|
while retries_left > 0:
|
||||||
|
retries_left -= 1
|
||||||
|
response = self._agent.completion(messages=messages)
|
||||||
|
if debug_dir:
|
||||||
|
with open(
|
||||||
|
f"{debug_dir}/{retries_left}-retries-left.txt",
|
||||||
|
"w",
|
||||||
|
encoding="utf-8",
|
||||||
|
) as f:
|
||||||
|
f.write(response)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = _BuildResult.model_validate_json(response)
|
||||||
|
self._validate_result(result, window)
|
||||||
|
return result
|
||||||
|
except (ValidationError, ValueError, TypeError):
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
raise RuntimeError("Agent has failed to provide valid schema too many times")
|
||||||
|
|
||||||
|
def build(self, timeline: Timeline) -> Structure:
|
||||||
|
"""Build the ordered structure of a future Markdown document."""
|
||||||
|
elements: list[StructureElement] = []
|
||||||
|
context = _BuilderContext()
|
||||||
|
|
||||||
|
for window in self._windowizer.windowize(timeline.events):
|
||||||
|
window.context = context.model_dump(mode="json")
|
||||||
|
intermediate = self._build_window(window)
|
||||||
|
elements.extend(intermediate.elements)
|
||||||
|
context = intermediate.context
|
||||||
|
|
||||||
|
return Structure(elements=elements)
|
||||||
Reference in New Issue
Block a user