pulse-engine 0.2.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.
Files changed (150) hide show
  1. pulse_engine/__init__.py +0 -0
  2. pulse_engine/adapters/__init__.py +58 -0
  3. pulse_engine/adapters/audio_transcription.py +167 -0
  4. pulse_engine/adapters/batcher.py +36 -0
  5. pulse_engine/adapters/digital_news.py +128 -0
  6. pulse_engine/adapters/digital_news_metadata.py +536 -0
  7. pulse_engine/adapters/exceptions.py +10 -0
  8. pulse_engine/adapters/models.py +134 -0
  9. pulse_engine/adapters/opensearch_storage.py +160 -0
  10. pulse_engine/adapters/speech_content.py +130 -0
  11. pulse_engine/adapters/speech_metadata.py +374 -0
  12. pulse_engine/adapters/twitter.py +423 -0
  13. pulse_engine/adapters/youtube_downloader.py +186 -0
  14. pulse_engine/adapters/youtube_metadata.py +261 -0
  15. pulse_engine/api/__init__.py +0 -0
  16. pulse_engine/api/v1/__init__.py +0 -0
  17. pulse_engine/api/v1/auth.py +91 -0
  18. pulse_engine/api/v1/health.py +62 -0
  19. pulse_engine/api/v1/router.py +16 -0
  20. pulse_engine/chain_recovery.py +131 -0
  21. pulse_engine/cli/__init__.py +0 -0
  22. pulse_engine/cli/main.py +169 -0
  23. pulse_engine/cli/templates/cookiecutter.json +4 -0
  24. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/.gitignore +13 -0
  25. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/Dockerfile +32 -0
  26. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pipeline.yaml +17 -0
  27. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/pyproject.toml +25 -0
  28. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/src/pulse_{{cookiecutter.product_slug}}/__init__.py +8 -0
  29. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/__init__.py +0 -0
  30. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/__init__.py +0 -0
  31. pulse_engine/cli/templates/pulse-{{cookiecutter.product_name}}/tests/unit/test_manifest.py +15 -0
  32. pulse_engine/client.py +95 -0
  33. pulse_engine/config.py +157 -0
  34. pulse_engine/core/__init__.py +0 -0
  35. pulse_engine/core/error_handlers.py +64 -0
  36. pulse_engine/core/exceptions.py +67 -0
  37. pulse_engine/core/job_token.py +109 -0
  38. pulse_engine/core/logging.py +45 -0
  39. pulse_engine/core/scope.py +23 -0
  40. pulse_engine/core/security.py +130 -0
  41. pulse_engine/database.py +30 -0
  42. pulse_engine/dependencies.py +166 -0
  43. pulse_engine/deployment/__init__.py +0 -0
  44. pulse_engine/deployment/backend_deployment_repository.py +83 -0
  45. pulse_engine/deployment/backends/__init__.py +0 -0
  46. pulse_engine/deployment/backends/base.py +50 -0
  47. pulse_engine/deployment/backends/exceptions.py +20 -0
  48. pulse_engine/deployment/backends/native_lambda.py +125 -0
  49. pulse_engine/deployment/backends/prefect_ecs.py +116 -0
  50. pulse_engine/deployment/backends/prefect_k8s.py +131 -0
  51. pulse_engine/deployment/backends/registry.py +50 -0
  52. pulse_engine/deployment/infra_provisioner.py +285 -0
  53. pulse_engine/deployment/job_launcher.py +178 -0
  54. pulse_engine/deployment/models.py +48 -0
  55. pulse_engine/deployment/repository.py +54 -0
  56. pulse_engine/deployment/router.py +22 -0
  57. pulse_engine/deployment/schemas.py +18 -0
  58. pulse_engine/deployment/service.py +65 -0
  59. pulse_engine/extractor/__init__.py +0 -0
  60. pulse_engine/extractor/adapters/__init__.py +0 -0
  61. pulse_engine/extractor/base.py +48 -0
  62. pulse_engine/extractor/models.py +50 -0
  63. pulse_engine/extractor/orchestrator/__init__.py +15 -0
  64. pulse_engine/extractor/orchestrator/base.py +34 -0
  65. pulse_engine/extractor/orchestrator/noop.py +37 -0
  66. pulse_engine/extractor/orchestrator/prefect.py +163 -0
  67. pulse_engine/extractor/repository.py +163 -0
  68. pulse_engine/extractor/router.py +102 -0
  69. pulse_engine/extractor/schemas.py +93 -0
  70. pulse_engine/extractor/service.py +431 -0
  71. pulse_engine/extractor/stage_models.py +36 -0
  72. pulse_engine/extractor/stage_repository.py +109 -0
  73. pulse_engine/main.py +195 -0
  74. pulse_engine/mcp/__init__.py +0 -0
  75. pulse_engine/mcp/__main__.py +5 -0
  76. pulse_engine/mcp/server.py +108 -0
  77. pulse_engine/mcp/tools_jobs.py +159 -0
  78. pulse_engine/mcp/tools_kb.py +88 -0
  79. pulse_engine/mcp/tools_modules.py +115 -0
  80. pulse_engine/mcp/tools_pipelines.py +215 -0
  81. pulse_engine/mcp/tools_processor.py +208 -0
  82. pulse_engine/middleware/__init__.py +0 -0
  83. pulse_engine/middleware/rate_limit.py +144 -0
  84. pulse_engine/middleware/request_id.py +16 -0
  85. pulse_engine/middleware/security_headers.py +25 -0
  86. pulse_engine/middleware/tenant.py +90 -0
  87. pulse_engine/pipeline/__init__.py +0 -0
  88. pulse_engine/pipeline/config_parser.py +148 -0
  89. pulse_engine/pipeline/expression.py +268 -0
  90. pulse_engine/pipeline/models.py +98 -0
  91. pulse_engine/pipeline/repositories.py +224 -0
  92. pulse_engine/pipeline/router_modules.py +66 -0
  93. pulse_engine/pipeline/router_pipelines.py +198 -0
  94. pulse_engine/pipeline/schemas.py +200 -0
  95. pulse_engine/pipeline/service.py +250 -0
  96. pulse_engine/pipeline/translators/__init__.py +44 -0
  97. pulse_engine/pipeline/translators/airflow_status.py +11 -0
  98. pulse_engine/pipeline/translators/airflow_translator.py +22 -0
  99. pulse_engine/pipeline/translators/base.py +42 -0
  100. pulse_engine/pipeline/translators/prefect_status.py +93 -0
  101. pulse_engine/pipeline/translators/prefect_translator.py +195 -0
  102. pulse_engine/processor/__init__.py +0 -0
  103. pulse_engine/processor/base.py +36 -0
  104. pulse_engine/processor/core/__init__.py +0 -0
  105. pulse_engine/processor/core/analysis.py +148 -0
  106. pulse_engine/processor/core/chunking.py +158 -0
  107. pulse_engine/processor/core/prompts.py +340 -0
  108. pulse_engine/processor/core/topic_splitter.py +105 -0
  109. pulse_engine/processor/defaults/__init__.py +11 -0
  110. pulse_engine/processor/defaults/core_processor.py +12 -0
  111. pulse_engine/processor/defaults/postprocessor.py +12 -0
  112. pulse_engine/processor/defaults/preprocessor.py +12 -0
  113. pulse_engine/processor/llm/__init__.py +0 -0
  114. pulse_engine/processor/llm/provider.py +58 -0
  115. pulse_engine/processor/ocr/gemini.py +52 -0
  116. pulse_engine/processor/pipeline.py +107 -0
  117. pulse_engine/processor/postprocessor/__init__.py +0 -0
  118. pulse_engine/processor/postprocessor/embeddings.py +34 -0
  119. pulse_engine/processor/postprocessor/tasks.py +180 -0
  120. pulse_engine/processor/preprocessor/__init__.py +0 -0
  121. pulse_engine/processor/preprocessor/tasks.py +71 -0
  122. pulse_engine/processor/router.py +192 -0
  123. pulse_engine/processor/schemas.py +167 -0
  124. pulse_engine/registry.py +117 -0
  125. pulse_engine/runners/__init__.py +0 -0
  126. pulse_engine/runners/lambda_runner.py +26 -0
  127. pulse_engine/runners/pipeline_runner.py +43 -0
  128. pulse_engine/runners/prefect_pipeline_flow.py +904 -0
  129. pulse_engine/runners/prefect_runner.py +33 -0
  130. pulse_engine/s3.py +72 -0
  131. pulse_engine/secrets.py +46 -0
  132. pulse_engine/services/__init__.py +0 -0
  133. pulse_engine/services/bootstrap.py +211 -0
  134. pulse_engine/services/opensearch.py +84 -0
  135. pulse_engine/storage/__init__.py +0 -0
  136. pulse_engine/storage/connectors/__init__.py +0 -0
  137. pulse_engine/storage/connectors/athena.py +226 -0
  138. pulse_engine/storage/connectors/base.py +32 -0
  139. pulse_engine/storage/connectors/opensearch.py +344 -0
  140. pulse_engine/storage/knowledge_base.py +68 -0
  141. pulse_engine/storage/router.py +78 -0
  142. pulse_engine/storage/schemas.py +93 -0
  143. pulse_engine/testing/__init__.py +13 -0
  144. pulse_engine/testing/fixtures.py +50 -0
  145. pulse_engine/testing/mocks.py +104 -0
  146. pulse_engine/worker.py +53 -0
  147. pulse_engine-0.2.0.dist-info/METADATA +654 -0
  148. pulse_engine-0.2.0.dist-info/RECORD +150 -0
  149. pulse_engine-0.2.0.dist-info/WHEEL +4 -0
  150. pulse_engine-0.2.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,36 @@
1
+ """Base ABCs for the three-stage processing pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any
7
+
8
+ from pulse_engine.processor.schemas import OCRInput, ProcessingContext
9
+
10
+
11
+ class BasePreprocessor(ABC):
12
+ """Preprocessing stage: cleaning, normalisation, language detection."""
13
+
14
+ @abstractmethod
15
+ def process(self, ctx: ProcessingContext) -> ProcessingContext: ...
16
+
17
+
18
+ class BaseCoreProcessor(ABC):
19
+ """Core processing stage: chunking, NER, sentiment, topics, summarisation."""
20
+
21
+ @abstractmethod
22
+ def process(self, ctx: ProcessingContext) -> ProcessingContext: ...
23
+
24
+
25
+ class BasePostprocessor(ABC):
26
+ """Post-processing stage: embeddings, quality scoring, dedup, storage formatting."""
27
+
28
+ @abstractmethod
29
+ def process(self, ctx: ProcessingContext) -> ProcessingContext: ...
30
+
31
+
32
+ class BaseOCRProvider(ABC):
33
+ """Base class that all OCR providers must implement."""
34
+
35
+ @abstractmethod
36
+ def extract(self, ocr_input: OCRInput) -> Any: ...
File without changes
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from pulse_engine.processor.core.chunking import chunk_content
6
+ from pulse_engine.processor.schemas import (
7
+ Entity,
8
+ ProcessingContext,
9
+ ProcessingError,
10
+ )
11
+
12
+ # Simple stop words for topic extraction
13
+ _STOP_WORDS = frozenset(
14
+ "the a an and or but in on at to for of is it this that with from by as are was "
15
+ "were be been being have has had do does did will would could should may might can "
16
+ "shall not no nor so if then than too very just about also more other some such "
17
+ "only over after before between through during each every all both few most own "
18
+ "same any many much how what which who whom when where why again further once here "
19
+ "there their them they he she his her its our your we you i me my".split()
20
+ )
21
+
22
+ _POSITIVE_WORDS = frozenset(
23
+ "good great excellent amazing wonderful fantastic positive happy love beautiful "
24
+ "best brilliant superb outstanding perfect delightful pleasant remarkable "
25
+ "awesome terrific marvelous fabulous incredible impressive magnificent".split()
26
+ )
27
+
28
+ _NEGATIVE_WORDS = frozenset(
29
+ "bad terrible awful horrible negative sad hate ugly worst poor pathetic "
30
+ "dreadful disappointing unpleasant atrocious abysmal horrendous disgusting "
31
+ "miserable wretched appalling lousy inferior".split()
32
+ )
33
+
34
+
35
+ def extract_entities(text: str) -> list[Entity]:
36
+ """Extract entities using regex patterns: capitalized multi-word names, dates."""
37
+ entities: list[Entity] = []
38
+ seen: set[str] = set()
39
+
40
+ # Find capitalized multi-word sequences (2+ words starting with uppercase)
41
+ for m in re.finditer(r"\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)+)\b", text):
42
+ name = m.group(1)
43
+ if name not in seen:
44
+ seen.add(name)
45
+ entities.append(
46
+ Entity(name=name, type="PERSON", start=m.start(), end=m.end())
47
+ )
48
+
49
+ # Find dates in YYYY-MM-DD format
50
+ for m in re.finditer(r"\b(\d{4}-\d{2}-\d{2})\b", text):
51
+ date_str = m.group(1)
52
+ if date_str not in seen:
53
+ seen.add(date_str)
54
+ entities.append(
55
+ Entity(name=date_str, type="DATE", start=m.start(), end=m.end())
56
+ )
57
+
58
+ return entities
59
+
60
+
61
+ def classify_sentiment(text: str) -> str:
62
+ """Classify sentiment using keyword matching: positive/negative/neutral."""
63
+ words = set(re.findall(r"\b[a-z]+\b", text.lower()))
64
+ pos_count = len(words & _POSITIVE_WORDS)
65
+ neg_count = len(words & _NEGATIVE_WORDS)
66
+
67
+ if pos_count > neg_count:
68
+ return "positive"
69
+ elif neg_count > pos_count:
70
+ return "negative"
71
+ return "neutral"
72
+
73
+
74
+ def extract_topics(text: str, max_topics: int = 5) -> list[str]:
75
+ """Extract topics based on most frequent significant words."""
76
+ words = re.findall(r"\b[a-z]{3,}\b", text.lower())
77
+ filtered = [w for w in words if w not in _STOP_WORDS]
78
+
79
+ freq: dict[str, int] = {}
80
+ for w in filtered:
81
+ freq[w] = freq.get(w, 0) + 1
82
+
83
+ sorted_words = sorted(freq, key=freq.__getitem__, reverse=True)
84
+ return sorted_words[:max_topics]
85
+
86
+
87
+ def summarize(text: str, max_sentences: int = 3) -> str:
88
+ """Extractive summarization: return the first N sentences."""
89
+ sentences = re.split(r"(?<=[.!?])\s+", text.strip())
90
+ sentences = [s.strip() for s in sentences if s.strip()]
91
+ selected = sentences[:max_sentences]
92
+ return " ".join(selected) if selected else text
93
+
94
+
95
+ def run_core_processing(ctx: ProcessingContext) -> ProcessingContext:
96
+ """Run chunking and analysis on the processing context."""
97
+ try:
98
+ content = ctx.cleaned_content or ctx.raw_content
99
+
100
+ # Chunking
101
+ ctx.chunks = chunk_content(
102
+ text=content,
103
+ source_id=ctx.source_id,
104
+ strategy=ctx.options.chunk_strategy,
105
+ chunk_size=ctx.options.chunk_size,
106
+ overlap=ctx.options.chunk_overlap,
107
+ )
108
+
109
+ # Analyze each chunk
110
+ all_entities: list[Entity] = []
111
+ for chunk in ctx.chunks:
112
+ if ctx.options.enable_ner:
113
+ chunk.entities = extract_entities(chunk.content)
114
+ all_entities.extend(chunk.entities)
115
+
116
+ if ctx.options.enable_sentiment:
117
+ chunk.sentiment = classify_sentiment(chunk.content)
118
+
119
+ if ctx.options.enable_topics:
120
+ chunk.topics = extract_topics(chunk.content)
121
+
122
+ # Global analysis on full content
123
+ if ctx.options.enable_ner:
124
+ ctx.entities = extract_entities(content)
125
+ # Merge with chunk-level entities (deduplicated)
126
+ seen = {e.name for e in ctx.entities}
127
+ for e in all_entities:
128
+ if e.name not in seen:
129
+ seen.add(e.name)
130
+ ctx.entities.append(e)
131
+
132
+ if ctx.options.enable_sentiment:
133
+ ctx.sentiment = classify_sentiment(content)
134
+
135
+ if ctx.options.enable_topics:
136
+ ctx.topics = extract_topics(content)
137
+
138
+ ctx.summary = summarize(content)
139
+
140
+ ctx.stages_completed.append("core_processing")
141
+ except Exception as e:
142
+ ctx.errors.append(
143
+ ProcessingError(
144
+ stage="core_processing", task="run_core_processing", message=str(e)
145
+ )
146
+ )
147
+
148
+ return ctx
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Callable
5
+
6
+ import tiktoken
7
+
8
+ from pulse_engine.processor.schemas import ContentChunk
9
+
10
+ _encoding: tiktoken.Encoding | None = None
11
+
12
+
13
+ def _get_encoding() -> tiktoken.Encoding:
14
+ global _encoding
15
+ if _encoding is None:
16
+ _encoding = tiktoken.get_encoding("cl100k_base")
17
+ return _encoding
18
+
19
+
20
+ def _count_tokens(text: str) -> int:
21
+ return len(_get_encoding().encode(text))
22
+
23
+
24
+ def chunk_content(
25
+ text: str,
26
+ source_id: str,
27
+ strategy: str = "token_count",
28
+ chunk_size: int = 512,
29
+ overlap: int = 50,
30
+ ) -> list[ContentChunk]:
31
+ """Dispatch to the appropriate chunking strategy."""
32
+ if not text or not text.strip():
33
+ return []
34
+
35
+ def _token_chunker(t: str, sid: str) -> list[ContentChunk]:
36
+ return _chunk_by_token_count(t, sid, chunk_size, overlap)
37
+
38
+ def _sliding_chunker(t: str, sid: str) -> list[ContentChunk]:
39
+ return _chunk_by_sliding_window(t, sid, chunk_size, overlap)
40
+
41
+ strategies: dict[str, Callable[[str, str], list[ContentChunk]]] = {
42
+ "paragraph": _chunk_by_paragraph,
43
+ "token_count": _token_chunker,
44
+ "sentence": _chunk_by_sentence,
45
+ "sliding_window": _sliding_chunker,
46
+ }
47
+
48
+ chunker = strategies.get(strategy)
49
+ if chunker is None:
50
+ chunker = _token_chunker
51
+
52
+ return chunker(text, source_id)
53
+
54
+
55
+ def _chunk_by_paragraph(
56
+ text: str, source_id: str, chunk_size: int = 512
57
+ ) -> list[ContentChunk]:
58
+ """Split text on double newlines into paragraph-based chunks."""
59
+ paragraphs = re.split(r"\n\n+", text.strip())
60
+ paragraphs = [p.strip() for p in paragraphs if p.strip()]
61
+
62
+ if not paragraphs:
63
+ return []
64
+
65
+ chunks: list[ContentChunk] = []
66
+ for i, para in enumerate(paragraphs):
67
+ chunks.append(
68
+ ContentChunk(
69
+ chunk_index=i,
70
+ content=para,
71
+ token_count=_count_tokens(para),
72
+ parent_source_id=source_id,
73
+ )
74
+ )
75
+ return chunks
76
+
77
+
78
+ def _chunk_by_token_count(
79
+ text: str, source_id: str, chunk_size: int = 512, overlap: int = 50
80
+ ) -> list[ContentChunk]:
81
+ """Chunk by token count using tiktoken cl100k_base encoding."""
82
+ tokens = _get_encoding().encode(text)
83
+ if not tokens:
84
+ return []
85
+
86
+ chunks: list[ContentChunk] = []
87
+ start = 0
88
+ idx = 0
89
+
90
+ while start < len(tokens):
91
+ end = min(start + chunk_size, len(tokens))
92
+ chunk_tokens = tokens[start:end]
93
+ chunk_text = _get_encoding().decode(chunk_tokens)
94
+ chunks.append(
95
+ ContentChunk(
96
+ chunk_index=idx,
97
+ content=chunk_text,
98
+ token_count=len(chunk_tokens),
99
+ parent_source_id=source_id,
100
+ )
101
+ )
102
+ idx += 1
103
+ if end >= len(tokens):
104
+ break
105
+ # Ensure forward progress: overlap must be less than chunk_size
106
+ effective_overlap = min(overlap, chunk_size - 1)
107
+ start = end - effective_overlap
108
+
109
+ return chunks
110
+
111
+
112
+ def _chunk_by_sentence(text: str, source_id: str) -> list[ContentChunk]:
113
+ """Split text into sentence-level chunks."""
114
+ sentences = re.split(r"(?<=[.!?])\s+", text.strip())
115
+ sentences = [s.strip() for s in sentences if s.strip()]
116
+
117
+ chunks: list[ContentChunk] = []
118
+ for i, sentence in enumerate(sentences):
119
+ chunks.append(
120
+ ContentChunk(
121
+ chunk_index=i,
122
+ content=sentence,
123
+ token_count=_count_tokens(sentence),
124
+ parent_source_id=source_id,
125
+ )
126
+ )
127
+ return chunks
128
+
129
+
130
+ def _chunk_by_sliding_window(
131
+ text: str, source_id: str, chunk_size: int = 512, overlap: int = 50
132
+ ) -> list[ContentChunk]:
133
+ """Sliding window chunking with overlap (token-based)."""
134
+ tokens = _get_encoding().encode(text)
135
+ if not tokens:
136
+ return []
137
+
138
+ chunks: list[ContentChunk] = []
139
+ step = max(chunk_size - overlap, 1)
140
+ idx = 0
141
+
142
+ for start in range(0, len(tokens), step):
143
+ end = min(start + chunk_size, len(tokens))
144
+ chunk_tokens = tokens[start:end]
145
+ chunk_text = _get_encoding().decode(chunk_tokens)
146
+ chunks.append(
147
+ ContentChunk(
148
+ chunk_index=idx,
149
+ content=chunk_text,
150
+ token_count=len(chunk_tokens),
151
+ parent_source_id=source_id,
152
+ )
153
+ )
154
+ idx += 1
155
+ if end >= len(tokens):
156
+ break
157
+
158
+ return chunks
@@ -0,0 +1,340 @@
1
+ # ruff: noqa: E501
2
+ TRANSCRIPT_SPLITTER_EXAMPLES = """**Example 1:**
3
+ transcript:
4
+ [
5
+ (1, "Emily, 2025-01-20T13:59:50Z => Hi Robert, hope you're doing well today."),
6
+ (2, "Robert, 2025-01-20T13:59:55Z => Hi Emily, I'm doing great, thanks! How about you?"),
7
+ (3, "Emily, 2025-01-20T14:00:01Z => I'm good, thanks. Our January performance data shows improved CPCs and stable ROAS."),
8
+ (4, "Robert, 2025-01-20T14:00:05Z => The generator campaign delivered a 20% increase in impressions."),
9
+ (5, "Emily, 2025-01-20T14:00:11Z => Should we allocate more budget to capitalize on this trend?"),
10
+ (6, "Robert, 2025-01-20T14:00:15Z => Yes, I'll add a new generator SKU to our campaign."),
11
+ (7, "Emily, 2025-01-20T14:00:21Z => Inventory levels are robust, so extra budget makes sense."),
12
+ (8, "Robert, 2025-01-20T14:00:25Z => I'll update our bid strategy and reallocate funds from underperforming items."),
13
+ (9, "Emily, 2025-01-20T14:00:31Z => Let's closely monitor the updated CPC and ROAS."),
14
+ (10, "Robert, 2025-01-20T14:00:35Z => I'll track performance and adjust bids as needed."),
15
+ (11, "Emily, 2025-01-20T14:00:41Z => Great, we'll reconvene next week to review the impact."),
16
+ (12, "Robert, 2025-01-20T14:00:45Z => Agreed. I'll send over the updated campaign metrics.")
17
+ ]
18
+
19
+
20
+ output :
21
+ ```json
22
+ {{{{
23
+ "topic_shifts" : [
24
+ {{{{
25
+ "index": 3,
26
+ "reason": "Shift from introductory greetings to discussing performance metrics."
27
+ }}}},
28
+ {{{{
29
+ "index": 5,
30
+ "reason": "Shift from discussing performance metrics and campaign results to budget allocation and campaign modifications."
31
+ }}}},
32
+ {{{{
33
+ "index": 11,
34
+ "reason": "Transition from budget discussion to scheduling a follow-up review call."
35
+ }}}}
36
+ ]
37
+ }}}}
38
+ ```
39
+
40
+ **Example 2:**
41
+ transcript:
42
+ [
43
+ (1, "Alex, 2025-02-10T10:59:50Z => so that"),
44
+ (2, "Alex, 2025-02-10T10:59:50Z => Hey Taylor, how's your morning going?"),
45
+ (3, "Taylor, 2025-02-10T10:59:55Z => Busy but good, Alex! Ready to dive in."),
46
+ (4, "Alex, 2025-02-10T11:00:02Z => Perfect. Let's review our current campaign strategy and segmentation."),
47
+ (5, "Taylor, 2025-02-10T11:00:05Z => I've noticed several ad groups are underperforming."),
48
+ (6, "Alex, 2025-02-10T11:00:10Z => The sponsored video campaign isn't engaging our audience well."),
49
+ (7, "Taylor, 2025-02-10T11:00:13Z => I propose testing alternative creatives for the SV ads."),
50
+ (8, "Alex, 2025-02-10T11:00:18Z => We should reallocate the budget from low performers to our top sellers."),
51
+ (9, "Taylor, 2025-02-10T11:00:21Z => I'm setting up A/B tests for the new SV creative approaches."),
52
+ (10, "Alex, 2025-02-10T11:00:26Z => When can we expect preliminary results?"),
53
+ (11, "Taylor, 2025-02-10T11:00:29Z => Initial data should be ready within three days."),
54
+ (12, "Alex, 2025-02-10T11:00:34Z => Let's schedule a review call after the tests are complete."),
55
+ (13, "Taylor, 2025-02-10T11:00:37Z => Agreed. I'll coordinate the meeting and share test outcomes.")
56
+ ]
57
+
58
+
59
+ output :
60
+ ```json
61
+ {{{{
62
+ "topic_shifts" : [
63
+ {{{{
64
+ "index": 4,
65
+ "reason": "Shift from greetings to discussing campaign strategy and segmentation."
66
+ }}}},
67
+ {{{{
68
+ "index": 8,
69
+ "reason": "Shift from discussing campaign strategy and creative testing to budget reallocation for top sellers."
70
+ }}}},
71
+ {{{{
72
+ "index": 10,
73
+ "reason": "Transition from budget reallocation to discussing test timelines and scheduling a review call."
74
+ }}}}
75
+ ]
76
+ }}}}
77
+ ```
78
+ """
79
+
80
+ TRANSCRIPT_SPLITTER_PROMPT = """**Role:** Assume you are an expert meeting transcript summarizer specialized in identifying topic switches happening in the transcript.
81
+
82
+ **Inputs:**
83
+ **transcript:** A sequence of strings in the following format: Index number followed by speaker name followed by a label to indicate which business entity they belong to (for ex: account or client or guest or name of a company)[Account/client/guest - buisness entity]. This will then be followed by a start and end time stamp and finally the utterance by the speaker. You will be provided with these strings for a full meeting discussion.
84
+ * **Transcript Format:** `Speaker Name [Label - Entity], StartTime, EndTime => Utterance`
85
+ **Interaction details: The interaction {interaction_title} is between business entity {account} and business entity {client}. The attendees from the account side are {account_attendees} and the attendees from the client/prospect/vendor/partner side are {client_attendees}.
86
+ **Note - that the entities involved in the conversation should not be interchanged while generating the topics.
87
+
88
+ **Task:** Your task is twofold:
89
+ 1. First, generate clear, concise topics from the provided meeting transcript. Each topic should capture one distinct discussion agendas.
90
+ 2. Then, analyze the transcript to determine the exact indexes where there is a shift between the identified topics.
91
+
92
+ **Instructions to generate distinct discussion agendas**:
93
+ 1. Read the transcript in chronological order.
94
+ 2. Maintain a running understanding of the CURRENT TOPIC.
95
+ 3. For each new utterance:
96
+ - Decide whether it CONTINUES the current topic, or
97
+ - INTRODUCES A NEW TOPIC.
98
+ 4. A new topic should ONLY be created if the discussion theme clearly changes.
99
+ 5. Do NOT create a new topic for:
100
+ - Short acknowledgements or confirmations
101
+ - Examples or clarifications within the same discussion
102
+ - Repetition or rephrasing of the same idea
103
+ 6. All the distinct discussion agendas must be captured.
104
+
105
+ **Examples:**
106
+ {examples}
107
+
108
+ **Guidelines:**
109
+ 1. **Topic Generation:**
110
+ - Extract clear and concise topics that capture generic, high-level themes from the transcript.
111
+ - Avoid overly specific keypoints; instead, group related details into broad discussion topics within the context.
112
+ - Each topic item should capture the essence of the discussion.
113
+
114
+ 2. **Topic Shift Identification:**
115
+ - Identify the transcript indexes where the discussion shifts from one agenda to another.
116
+ - Use the exact `index` numbers from the input transcript tuples.
117
+ - The `index` value should not be more than the maximum value of index from input tuple which is {max_idx}.
118
+ - Each identified shift should be explained briefly in the "reason" field.
119
+
120
+ **Transcript:**
121
+ {transcript}
122
+ """
123
+
124
+
125
+ TOPIC_GENERATOR_EXAMPLES = """Examples :
126
+ Example 1 :
127
+ transcript: ```
128
+ Alice (Sales Lead): Good morning everyone, I'm Alice from Acme Corp—thanks for joining.
129
+ Bob (CFO, Beta Inc): Hi Alice—Bob here. Also with me is Carol, our AR manager.
130
+ Carol (AR Manager): Morning! We handle all our invoicing and collections in-house right now.
131
+ Alice: Got it. To kick off, what are your biggest pain points with that process today?
132
+ Bob: It's entirely manual—spreadsheets, email threads, no visibility. We miss follow-ups and collections lag by 15+ days.
133
+ Carol: And we don't have a unified dashboard, so leadership can't track daily cash inflows.
134
+ Alice: Understood. Our solution automates invoice generation, sends smart reminders, and offers real-time cash-flow dashboards.
135
+ Alice: Does that address the issues you just described?
136
+ Bob: Absolutely—that's exactly what we need.
137
+ Alice: Great. Would you be open to a tailored demo next Wednesday at 10 AM?
138
+ Bob: That works. Please send an invite. ```
139
+ Interaction_type: Sales_Discovery_Call
140
+ Playbook : ```Introductions - who is present in the Interaction
141
+ Probing for pain point in the prospect
142
+ Providing a quick overview of the Product and Capabilities
143
+ Confirming the pain point experienced by the prospect
144
+ Gauging the interest in moving to the next step
145
+ Setting Clear Next Steps with Dates```
146
+
147
+ output : [
148
+ "Introductions and Attendees",
149
+ "Pain Points: Manual invoicing, email-based follow-ups, lack of visibility",
150
+ "Solution Overview: Invoice automation, smart reminders, dashboards",
151
+ "Solution Fit Validation",
152
+ "Next-Step Commitment"
153
+ ]
154
+
155
+ Example 2 :
156
+ transcript: ```
157
+ Chris: Good morning team—let's run through our sprint items.
158
+ Dana: Yesterday I finished the homepage redesign and updated the style guide.
159
+ Eli: API integration is 80% complete; I need specs for the new endpoint.
160
+ Fran: QA passed the login workflow, but there's a minor bug on password reset.
161
+ Chris: Noted. Any blockers?
162
+ Dana: Waiting on final copy from content.
163
+ Eli: No blockers on my end.
164
+ Fran: I need test user credentials from IT.
165
+ Chris: Deal—I'll follow up after this call.
166
+ ```
167
+ Interaction_type: Sales_Discovery_Call
168
+ Playbook : ```Introductions - who is present in the Interaction
169
+ Probing for pain point in the prospect
170
+ Providing a quick overview of the Product and Capabilities
171
+ Confirming the pain point experienced by the prospect
172
+ Gauging the interest in moving to the next step
173
+ Setting Clear Next Steps with Dates```
174
+
175
+ output : [
176
+ "Sprint Progress & Outcomes",
177
+ "Risks, Blockers & Dependencies",
178
+ "Follow-Up Actions"
179
+ ]
180
+ """
181
+
182
+ TOPIC_GENERATOR_PROMPT = """**Role:**
183
+ You are an expert language model specialized in extracting and summarizing clear, concise discussion topics from meeting transcripts.
184
+
185
+ **Inputs:**
186
+
187
+ * **Transcript:** A sequence of strings in this format: There will be a speaker name followed by a label to indicate which business entity they belong to (for ex: account or client or guest or name of a company)[Account/client/guest - buisness entity]. This will then be followed by a start and end time stamp and finally the utterance by the speaker. You will be provided with these strings for a full meeting discussion.
188
+ * **Transcript Format:** `Speaker Name [Label - Entity], StartTime, EndTime => Utterance`
189
+ * **Interaction Type:**
190
+ Examples include:
191
+
192
+ * Sales: `Introductory_Discovery_Call`, `Demo_Call`, `Follow_Up_Call`, `Finalization_Close_Call`
193
+ * Services: `Recurring_Update`, `performance_update_call`, `Problem_Resolution_Call`, `Implementation_Call`
194
+ * Generic: `generic`
195
+
196
+ * **Playbook:** A predefined list of agenda topics related to the specified interaction type.
197
+
198
+ **Interaction details: The interaction {{interaction_title}} is between business entity account: {{account}} and business entity
199
+ client/prospect/vendor/partner :{{client}}. The attendees from the account side are {{account_attendees}} and the attendees from the
200
+ client/prospect/vendor/partner side are {{client_attendees}}.
201
+ **Note - that the entities involved in the conversation should not be interchanged while generating the topics.
202
+
203
+ **Task:**
204
+ **Instructions to generate distinct discussion agendas**:
205
+ 1. Read the complete transcript in chronological order.
206
+ 2. You are given the business entities involved in the discussion so identify and maintain the entities involved in the conversation. While generating the topics do not interchange the business entities involved in the conversation
207
+ 3. Determine the interaction type on the basis of agendas discussed in the conversation.
208
+ 4. Now Generate the topics on the basis of interaction type that best summarize the distinct discussion agendas from the transcript.
209
+ 5. while generating the topics do not interchange the entities involved in the conversation
210
+
211
+ * Identify and clearly extract a concise list of distinct topics discussed in the provided transcript.
212
+ * Topics may or may not match exactly with items listed in the provided playbook; both scenarios are acceptable.
213
+
214
+ Examples : {{examples}}
215
+
216
+
217
+ **Guidelines:**
218
+
219
+ * Each topic should summarize a distinct, meaningful segment of the discussion.
220
+ ***Limit the total number of topics to 3-8.**
221
+ ***Merge overlapping or closely related points into single high-level topics.**
222
+ * Avoid overly specific details or fragmented items; group related details into broader, logical topics.
223
+ """
224
+
225
+
226
+ TOPIC_AND_EXCERPT_MAPPER_PROMPT = """**Role:**
227
+ You are an expert classification model specialized in mapping excerpts from meeting transcripts to predefined discussion topics.
228
+
229
+ **Inputs:**
230
+
231
+ * **Excerpt:** A single text excerpt from a meeting transcript which is a discussion topic from the meeting and each line of the excerpt will be a speaker name followed by a label to indicate which business entity they belong to (for ex: account or client or guest or name of a company)[Account/client/guest - buisness entity]. This will then be followed by a start and end time stamp and finally the utterance by the speaker.
232
+ * **Excerpt Format:** `Speaker Name [Label - Entity], StartTime, EndTime => Utterance`
233
+ * **Topics:** A predefined list of topics.
234
+
235
+ **Interaction details: The interaction {{interaction_title}} is between business entity {{account}} and business entity {{client}}. The attendees from the account side are {{account_attendees}} and the attendees from the client/prospect/vendor/partner side are {{client_attendees}}.
236
+ **Note - that the entities involved in the conversation should not be interchanged while mapping the excerpt to topics.
237
+
238
+ **Task:**
239
+ * Read the excerpt carefully and identify its primary intent.
240
+ * Select the single topic that best and most completely represents the intent of the excerpt.
241
+ * Classify the excerpt strictly based on the central theme of its content.
242
+ * Ensure the excerpt aligns clearly and fully with the thematic focus of exactly one of the provided topics.
243
+ * Only assign multiple topics if the excerpt explicitly and unquestionably addresses more than one distinct theme.
244
+ * If the excerpt's central theme does not match clearly and confidently to any of the provided topics, return an empty array (`[]`).
245
+
246
+ **Guidelines:**
247
+
248
+ * Prioritize thematic alignment over keyword matching.
249
+ * Assign topics only when the thematic relevance is absolutely certain.
250
+ * If there's any ambiguity or doubt regarding thematic alignment, omit the topic.
251
+
252
+ **Output Format (JSON):**
253
+
254
+ {{{{
255
+ "mapped_topics": ["<Topic_1>", "<Topic_2>", "..."] // empty array [] if no confident thematic mapping found
256
+ }}}}
257
+ """
258
+
259
+
260
+ EXCERPT_SPLITTER_EXAMPLES = """**Example 1:**
261
+ Topics: ["Greetings", "Performance Review", "Budget Allocation"]
262
+ Excerpt:
263
+ [
264
+ (1, "Hi Robert, hope you're doing well today."),
265
+ (2, "Hi Emily, I'm doing great, thanks! How about you?"),
266
+ (3, "Our January performance data shows improved CPCs and stable ROAS."),
267
+ (4, "The generator campaign delivered a 20% increase in impressions."),
268
+ (5, "Should we allocate more budget to capitalize on this trend?"),
269
+ (6, "Yes, I'll add a new generator SKU to our campaign."),
270
+ (7, "Inventory levels are robust, so extra budget makes sense."),
271
+ (8, "I'll update our bid strategy and reallocate funds from underperforming items."),
272
+ (9, "Let's closely monitor the updated CPC and ROAS."),
273
+ (10,"I'll track performance and adjust bids as needed."),
274
+ (11,"Great, we'll reconvene next week to review the impact."),
275
+ (12,"Agreed. I'll send over the updated campaign metrics.")
276
+ ]
277
+
278
+
279
+ output :
280
+ ```json
281
+ {{
282
+ "segments": [
283
+ {{ "start_index": 1, "end_index": 2, "topic": "Greetings" }},
284
+ {{ "start_index": 3, "end_index": 4, "topic": "Performance Review" }},
285
+ {{ "start_index": 5, "end_index": 12, "topic": "Budget Allocation" }}
286
+ ]
287
+ }}
288
+ ```
289
+
290
+ **Example 2:**
291
+ Topics: ["Project Kickoff", "Technical Issue", "Next Steps"]
292
+ Excerpt:
293
+ [
294
+ (1, "Let's review the project scope and deliverables."),
295
+ (2, "We need API integration with the payment gateway."),
296
+ (3, "Our dev server is down, error 502."),
297
+ (4, "Engineers are already investigating the 502 gateway timeout."),
298
+ (5, "Once it's back up, we'll resume the integration tests."),
299
+ (6, "Agreed—then we can draft the rollout plan."),
300
+ (7, "I'll schedule the next status call for Monday.")
301
+ ]
302
+
303
+ output :
304
+ ```json
305
+ {{
306
+ "segments": [
307
+ {{ "start_index": 1, "end_index": 2, "topic": "Project Kickoff" }},
308
+ {{ "start_index": 3, "end_index": 5, "topic": "Technical Issue" }},
309
+ {{ "start_index": 6, "end_index": 7, "topic": "Next Steps" }}
310
+ ]
311
+ }}
312
+ ```
313
+ """
314
+
315
+
316
+ EXCERPT_SPLITTER_PROMPT = """You are an `expert` meeting-transcript summarizer specialized in slicing a transcript into contiguous topic segments.
317
+
318
+ **Inputs:**
319
+ * **excerpt:** A list of (index: int, text: str) tuples a focused segment of the meeting which contains a speaker name followed by a label to indicate which business entity they belong to (for ex: account or client or guest or name of a company)[Account/client/guest - buisness entity]. This will then be followed by a start and end time stamp and finally the utterance by the speaker.
320
+ * Excerpt Format: `Speaker Name [Label - Entity], StartTime, EndTime => Utterance`
321
+ * **topics:** A list of strings which contains topics identified in that meeting conversation.
322
+
323
+ **Interaction details: The interaction {{interaction_title}} is between business entity {{account}} and business entity {{client}}. The attendees from the account side are {{account_attendees}} and the attendees from the client/prospect/vendor/partner side are {{client_attendees}}.
324
+ **Note - that the entities involved in the conversation should not be interchanged while segmenting the excerpt into topics.
325
+
326
+ **Task:**
327
+ Partition `excerpt` into contiguous segments such that each segment corresponds to one of the provided topics.
328
+ For each segment, return `start_index`, `end_index`, and the `topic` being discussed.
329
+
330
+ **Guidelines:**
331
+
332
+ * A segment begins when discussion clearly shifts into one of the topics and ends just before it shifts to another.
333
+ * If a message doesn't map to any topic, it should be grouped with the previous segment.
334
+ * Do not overlap segments; cover the transcript in order.
335
+ * Use the largest possible spans under a single topic before splitting.
336
+ * Never Make up any information.
337
+ * Do not repeat topics.
338
+
339
+ Examples : {{examples}}
340
+ """