openai-gabriel 1.0.1__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.
- gabriel/__init__.py +61 -0
- gabriel/_version.py +1 -0
- gabriel/api.py +2284 -0
- gabriel/cli/__main__.py +60 -0
- gabriel/core/__init__.py +7 -0
- gabriel/core/llm_client.py +34 -0
- gabriel/core/pipeline.py +18 -0
- gabriel/core/prompt_template.py +152 -0
- gabriel/prompts/__init__.py +1 -0
- gabriel/prompts/bucket_prompt.jinja2 +113 -0
- gabriel/prompts/classification_prompt.jinja2 +50 -0
- gabriel/prompts/codify_prompt.jinja2 +95 -0
- gabriel/prompts/comparison_prompt.jinja2 +60 -0
- gabriel/prompts/deduplicate_prompt.jinja2 +41 -0
- gabriel/prompts/deidentification_prompt.jinja2 +112 -0
- gabriel/prompts/extraction_prompt.jinja2 +61 -0
- gabriel/prompts/filter_prompt.jinja2 +31 -0
- gabriel/prompts/ideation_prompt.jinja2 +80 -0
- gabriel/prompts/merge_prompt.jinja2 +47 -0
- gabriel/prompts/paraphrase_prompt.jinja2 +17 -0
- gabriel/prompts/rankings_prompt.jinja2 +49 -0
- gabriel/prompts/ratings_prompt.jinja2 +50 -0
- gabriel/prompts/regional_analysis_prompt.jinja2 +40 -0
- gabriel/prompts/seed.jinja2 +43 -0
- gabriel/prompts/snippets.jinja2 +117 -0
- gabriel/tasks/__init__.py +63 -0
- gabriel/tasks/_attribute_utils.py +69 -0
- gabriel/tasks/bucket.py +432 -0
- gabriel/tasks/classify.py +562 -0
- gabriel/tasks/codify.py +1033 -0
- gabriel/tasks/compare.py +235 -0
- gabriel/tasks/debias.py +1460 -0
- gabriel/tasks/deduplicate.py +341 -0
- gabriel/tasks/deidentify.py +316 -0
- gabriel/tasks/discover.py +524 -0
- gabriel/tasks/extract.py +455 -0
- gabriel/tasks/filter.py +169 -0
- gabriel/tasks/ideate.py +782 -0
- gabriel/tasks/merge.py +464 -0
- gabriel/tasks/paraphrase.py +531 -0
- gabriel/tasks/rank.py +2041 -0
- gabriel/tasks/rate.py +347 -0
- gabriel/tasks/seed.py +465 -0
- gabriel/tasks/whatever.py +344 -0
- gabriel/utils/__init__.py +64 -0
- gabriel/utils/audio_utils.py +42 -0
- gabriel/utils/file_utils.py +464 -0
- gabriel/utils/image_utils.py +22 -0
- gabriel/utils/jinja.py +31 -0
- gabriel/utils/logging.py +86 -0
- gabriel/utils/mapmaker.py +304 -0
- gabriel/utils/media_utils.py +78 -0
- gabriel/utils/modality_utils.py +148 -0
- gabriel/utils/openai_utils.py +5470 -0
- gabriel/utils/parsing.py +282 -0
- gabriel/utils/passage_viewer.py +2557 -0
- gabriel/utils/pdf_utils.py +20 -0
- gabriel/utils/plot_utils.py +2881 -0
- gabriel/utils/prompt_utils.py +42 -0
- gabriel/utils/word_matching.py +158 -0
- openai_gabriel-1.0.1.dist-info/METADATA +443 -0
- openai_gabriel-1.0.1.dist-info/RECORD +67 -0
- openai_gabriel-1.0.1.dist-info/WHEEL +5 -0
- openai_gabriel-1.0.1.dist-info/entry_points.txt +2 -0
- openai_gabriel-1.0.1.dist-info/licenses/LICENSE +201 -0
- openai_gabriel-1.0.1.dist-info/licenses/NOTICE +13 -0
- openai_gabriel-1.0.1.dist-info/top_level.txt +1 -0
gabriel/cli/__main__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Command line interface for GABRIEL.
|
|
2
|
+
|
|
3
|
+
The CLI intentionally exposes a light-touch interface focused on discovery
|
|
4
|
+
and verification. It is designed to help users confirm the installation,
|
|
5
|
+
identify available helpers, and navigate toward the Python API for day-to-day
|
|
6
|
+
use. Full task execution is provided by the Python functions rather than the
|
|
7
|
+
CLI so that users can compose prompts, callbacks, and checkpoints in code.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Iterable, List, Optional
|
|
15
|
+
|
|
16
|
+
from gabriel import __version__
|
|
17
|
+
from gabriel import tasks as _tasks
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _iter_task_names() -> Iterable[str]:
|
|
21
|
+
"""Yield the public task helpers exposed by ``gabriel.tasks``."""
|
|
22
|
+
|
|
23
|
+
return sorted(_tasks.__all__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="gabriel",
|
|
29
|
+
description=(
|
|
30
|
+
"GABRIEL provides GPT-powered helpers for social-science analysis. "
|
|
31
|
+
"Use the Python API for full control; run `gabriel --list-tasks` "
|
|
32
|
+
"to see the available helpers."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--version",
|
|
37
|
+
action="version",
|
|
38
|
+
version=f"%(prog)s {__version__}",
|
|
39
|
+
help="Show the installed gabriel version and exit.",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"--list-tasks",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="List the Python helpers bundled with gabriel.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args(argv)
|
|
48
|
+
|
|
49
|
+
if args.list_tasks:
|
|
50
|
+
print("Available helpers (importable from `gabriel`):")
|
|
51
|
+
for name in _iter_task_names():
|
|
52
|
+
print(f"- {name}")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
parser.print_help()
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
sys.exit(main())
|
gabriel/core/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Abstract LLM client interface."""
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from ..utils.openai_utils import get_all_responses, get_response
|
|
7
|
+
class LLMClient(ABC):
|
|
8
|
+
"""Minimal interface for language model providers."""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def acall(self, messages: List[Dict[str, str]], **kwargs: Any) -> str:
|
|
12
|
+
"""Asynchronously call the LLM with provided messages."""
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
|
|
15
|
+
class OpenAIClient(LLMClient):
|
|
16
|
+
"""Concrete LLM client leveraging :func:`get_response`."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self, api_key: Optional[str] = None, base_url: Optional[str] = None
|
|
20
|
+
) -> None:
|
|
21
|
+
# ``get_response`` manages a shared client; API key via env or arg
|
|
22
|
+
if api_key:
|
|
23
|
+
os.environ.setdefault("OPENAI_API_KEY", api_key)
|
|
24
|
+
if base_url:
|
|
25
|
+
os.environ.setdefault("OPENAI_BASE_URL", base_url)
|
|
26
|
+
|
|
27
|
+
async def acall(self, messages: List[Dict[str, str]], **kwargs: Any) -> str:
|
|
28
|
+
prompt = "\n".join(m.get("content", "") for m in messages)
|
|
29
|
+
responses, _ = await get_response(prompt, **kwargs)
|
|
30
|
+
return responses[0] if responses else ""
|
|
31
|
+
|
|
32
|
+
async def get_all_responses(self, **kwargs: Any):
|
|
33
|
+
"""Convenience wrapper around :func:`get_all_responses`."""
|
|
34
|
+
return await get_all_responses(**kwargs)
|
gabriel/core/pipeline.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Pipeline primitives."""
|
|
2
|
+
from typing import Iterable, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Pipeline:
|
|
6
|
+
"""Very small placeholder for a processing pipeline."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, steps: Iterable[Any]):
|
|
9
|
+
self.steps = list(steps)
|
|
10
|
+
|
|
11
|
+
def run(self, data: Any) -> Any:
|
|
12
|
+
"""Run all steps sequentially."""
|
|
13
|
+
for step in self.steps:
|
|
14
|
+
if hasattr(step, "fit"):
|
|
15
|
+
step.fit(data)
|
|
16
|
+
if hasattr(step, "transform"):
|
|
17
|
+
data = step.transform(data)
|
|
18
|
+
return data
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Prompt template utilities."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import warnings
|
|
7
|
+
from typing import Dict, Optional, Set
|
|
8
|
+
|
|
9
|
+
from jinja2 import Environment, PackageLoader, Template, meta
|
|
10
|
+
|
|
11
|
+
from ..utils.jinja import shuffled, shuffled_dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class PromptTemplate:
|
|
16
|
+
"""Simple Jinja2-based prompt template."""
|
|
17
|
+
|
|
18
|
+
text: str
|
|
19
|
+
_environment: Environment = field(init=False, repr=False)
|
|
20
|
+
_template: Template = field(init=False, repr=False)
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
self._environment = Environment(loader=PackageLoader("gabriel", "prompts"))
|
|
24
|
+
self._environment.filters["shuffled_dict"] = shuffled_dict
|
|
25
|
+
self._environment.filters["shuffled"] = shuffled
|
|
26
|
+
self._template = self._environment.from_string(self.text)
|
|
27
|
+
|
|
28
|
+
def render(self, **params: Dict[str, str]) -> str:
|
|
29
|
+
"""Render the template with the given parameters."""
|
|
30
|
+
attrs = params.get("attributes")
|
|
31
|
+
descs = params.get("descriptions")
|
|
32
|
+
if isinstance(attrs, list):
|
|
33
|
+
if isinstance(descs, list) and len(descs) == len(attrs):
|
|
34
|
+
params["attributes"] = {a: d for a, d in zip(attrs, descs)}
|
|
35
|
+
else:
|
|
36
|
+
params["attributes"] = {a: a for a in attrs}
|
|
37
|
+
return self._template.render(**params)
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# Loading helpers
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _extract_variables(text: str) -> Set[str]:
|
|
45
|
+
"""Return the set of undeclared variables in ``text``.
|
|
46
|
+
|
|
47
|
+
A minimal Jinja environment is used that registers the filters
|
|
48
|
+
expected by the built-in prompts. This allows templates using
|
|
49
|
+
those filters to be parsed without error while ignoring their
|
|
50
|
+
actual behaviour.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
env = Environment()
|
|
54
|
+
env.filters["shuffled_dict"] = lambda x: x
|
|
55
|
+
env.filters["shuffled"] = lambda x: x
|
|
56
|
+
ast = env.parse(text)
|
|
57
|
+
return meta.find_undeclared_variables(ast)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_file(
|
|
61
|
+
cls,
|
|
62
|
+
path: str,
|
|
63
|
+
*,
|
|
64
|
+
reference_filename: Optional[str] = None,
|
|
65
|
+
package: str = "gabriel.prompts",
|
|
66
|
+
) -> "PromptTemplate":
|
|
67
|
+
"""Load a template from ``path`` with optional variable checking.
|
|
68
|
+
|
|
69
|
+
If ``reference_filename`` is provided, the custom template is
|
|
70
|
+
validated to ensure it declares the exact same set of variables
|
|
71
|
+
as the reference template. A descriptive ``ValueError`` is
|
|
72
|
+
raised if there is a mismatch.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
text = Path(path).read_text(encoding="utf-8")
|
|
76
|
+
if reference_filename is not None:
|
|
77
|
+
ref_text = resources.files(package).joinpath(reference_filename).read_text(
|
|
78
|
+
encoding="utf-8"
|
|
79
|
+
)
|
|
80
|
+
vars_custom = cls._extract_variables(text)
|
|
81
|
+
vars_ref = cls._extract_variables(ref_text)
|
|
82
|
+
missing = vars_ref - vars_custom
|
|
83
|
+
extra = vars_custom - vars_ref
|
|
84
|
+
if missing or extra:
|
|
85
|
+
parts = []
|
|
86
|
+
if missing:
|
|
87
|
+
parts.append(f"missing variables: {sorted(missing)}")
|
|
88
|
+
if extra:
|
|
89
|
+
parts.append(f"unexpected variables: {sorted(extra)}")
|
|
90
|
+
msg = "Custom template variable mismatch (" + "; ".join(parts) + ")"
|
|
91
|
+
required = {v for v in vars_ref if v in {"text", "attributes"}}
|
|
92
|
+
missing_required = required - vars_custom
|
|
93
|
+
if missing_required:
|
|
94
|
+
raise ValueError(msg)
|
|
95
|
+
warnings.warn(
|
|
96
|
+
msg + "; proceeding because required variables are present.",
|
|
97
|
+
UserWarning,
|
|
98
|
+
stacklevel=2,
|
|
99
|
+
)
|
|
100
|
+
return cls(text)
|
|
101
|
+
|
|
102
|
+
@classmethod
|
|
103
|
+
def from_package(
|
|
104
|
+
cls,
|
|
105
|
+
filename: str,
|
|
106
|
+
package: str = "gabriel.prompts",
|
|
107
|
+
) -> "PromptTemplate":
|
|
108
|
+
"""Load a template from the given package file."""
|
|
109
|
+
text = resources.files(package).joinpath(filename).read_text(encoding="utf-8")
|
|
110
|
+
return cls(text)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolve_template(
|
|
114
|
+
*,
|
|
115
|
+
template: Optional[PromptTemplate],
|
|
116
|
+
template_path: Optional[str],
|
|
117
|
+
reference_filename: str,
|
|
118
|
+
package: str = "gabriel.prompts",
|
|
119
|
+
) -> PromptTemplate:
|
|
120
|
+
"""Return a prompt template using either an object or a filesystem override.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
template:
|
|
125
|
+
Optional :class:`PromptTemplate` instance supplied directly by the caller.
|
|
126
|
+
template_path:
|
|
127
|
+
Filesystem path to a custom Jinja2 template. When provided, the template
|
|
128
|
+
is validated against ``reference_filename`` to ensure it exposes the same
|
|
129
|
+
variables as the built-in prompt.
|
|
130
|
+
reference_filename:
|
|
131
|
+
Name of the packaged template used as the default for the task. This is
|
|
132
|
+
used both as the fallback when no overrides are supplied and as the
|
|
133
|
+
reference for variable validation when ``template_path`` is set.
|
|
134
|
+
package:
|
|
135
|
+
Package containing the built-in prompt templates. Callers rarely need to
|
|
136
|
+
override this.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
if template is not None and template_path is not None:
|
|
140
|
+
raise ValueError("Provide either template or template_path, not both")
|
|
141
|
+
|
|
142
|
+
if template_path is not None:
|
|
143
|
+
template = PromptTemplate.from_file(
|
|
144
|
+
template_path,
|
|
145
|
+
reference_filename=reference_filename,
|
|
146
|
+
package=package,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return template or PromptTemplate.from_package(
|
|
150
|
+
reference_filename,
|
|
151
|
+
package=package,
|
|
152
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Your goal is to step back and propose exactly {{ bucket_count }} broader "bucket" terms that could serve as mutually exclusive groupings to capture many of these raw terms while still being specific.
|
|
2
|
+
Buckets often can be stylistically similar to the raw terms themselves (short phrases); can even be a raw term itself if that term already captures a broader grouping.
|
|
3
|
+
|
|
4
|
+
Each bucket should substantively and thematically group multiple raw terms.
|
|
5
|
+
Fine if some raw terms don't fit cleanly into any of your final buckets; aim though for your mutually exclusive buckets to jointly cover as many as reasonable.
|
|
6
|
+
Identify umbrella terms that group raw term subsets without losing important distinctions, specificity, and substance.
|
|
7
|
+
|
|
8
|
+
Below are the raw terms to bucketize.
|
|
9
|
+
BEGIN RAW TERMS
|
|
10
|
+
{{ terms }}
|
|
11
|
+
END RAW TERMS
|
|
12
|
+
|
|
13
|
+
We want exactly {{ bucket_count }} bucket terms.
|
|
14
|
+
They should together capture as many raw terms as is reasonable, without vagueness and still being mutually exclusive, descriptive, and specific.
|
|
15
|
+
Avoid buckets that are combinations of multiple distinct buckets, like "housing and employment issues" or "family and financial problems".
|
|
16
|
+
Each bucket should be a single coherent concept, specific and distinct from other buckets.
|
|
17
|
+
Ensure bucket terms have specificity and substance.
|
|
18
|
+
|
|
19
|
+
Really avoid vague buckets that are catch-alls, "miscellaneous", "singleton", or "other" categories.
|
|
20
|
+
If a potential bucket you have in mind is too vague and catch-all, consider a more specific term that captures a narrower grouping with more thematic precision to what those raw terms are about.
|
|
21
|
+
Strongly prefer descriptive, specific, and directional bucket terms (should be clear how a relevant piece of content could have more/less of the attribute).
|
|
22
|
+
No vague, overly broad, trivial, or overly abstract buckets.
|
|
23
|
+
Better to make a too specific than a too vague bucket, even if it means capturing fewer raw terms.
|
|
24
|
+
Final buckets must be meaningful, specific, interesting, and capturing the largest and most important clusters of raw terms.
|
|
25
|
+
|
|
26
|
+
Ensure mutual exclusivity-each bucket must be clearly distinct from each other bucket.
|
|
27
|
+
Do not select multiple conceptually overlapping buckets.
|
|
28
|
+
Rather think broadly and approach from different angles, noticing subtle hidden creative patterns that are important alongside the obvious ones.
|
|
29
|
+
We want broad coverage not just variations on the same idea, both obvious choices and subtle interesting patterns that one would only spot by scouring the data as you are.
|
|
30
|
+
Bucket diversity, mutual exclusivity, and creative exploration of different angles/subtle patterns are key.
|
|
31
|
+
We wish to identify both obvious and novel features/patterns that are non-obvious ex ante but significant in the actual data, requiring creative thinking to notice.
|
|
32
|
+
|
|
33
|
+
Final list must be exactly {{ bucket_count }} mutually exclusive, specific, and well constructed bucket terms.
|
|
34
|
+
|
|
35
|
+
{% if differentiate %}
|
|
36
|
+
Understand that these raw terms are differences that have been assessed as distinguishing features between two pieces of content, so consider bucket terms that maintain that spirit of distinguishing between two classes of content.
|
|
37
|
+
Specifically, realize that "square" and "circle" correspond to two classes of content as references.
|
|
38
|
+
So if raw terms you are grouping mention "circle" entries having an attribute or a "square" entry having an attribute more than "circle" entry, then those reference classes must be in the bucket name.
|
|
39
|
+
Crucial: you must distinguish between the the two classes of content. "square exhibits more X than circle" and "circle exhibits more X than square" do NOT mean the same thing and don't belong in the same bucket.
|
|
40
|
+
But "square exhibits more X" and "circle lacks X" DO mean the same thing and DO belong in the same bucket.
|
|
41
|
+
Ensure you are only bucketizing differences that actually mean the same thing; it is possible to even have both directions of the same attribute as two different buckets, if both versions are prevalent in the raw terms.
|
|
42
|
+
It is essential you mentally separate "square more X than circle" and "circle more X than square" instances regarding the same attribute X.
|
|
43
|
+
Consider each set by itself, and decide separately if each by itself merits a bucket.
|
|
44
|
+
|
|
45
|
+
Buckets must be clear on which class has more of the attribute (e.g. "square entries have more X than circle entries" or "circle entries lack Y relative to square entries").
|
|
46
|
+
Each bucket term should be a directional statement (e.g. "circle entries showcase stronger communication amongst colleagues than square entries", "square entries use lots of first person perspective while circle entries do not", "square entries invoke more chronic health issues circle entries", "circle entries involve more complications from type 2 diabetes than square entries", "square entries have few direct quotes from evidence relative to circle entries", etc) rather than an "X vs Y" phrase.
|
|
47
|
+
Closely follow the style of these examples; again, no "vs." etc; it must be clear and explicitly stated in bucket terms which group has more/less of the relevant attribute.
|
|
48
|
+
Focus on the most salient and common differences between the two classes of content, specifically as they appear in the raw terms.
|
|
49
|
+
TO BE CLEAR: all your bucket terms must explicitly state "square entries" and "circle entries", like "circle entries have more X than square entries", "square entries lack Y relative to circle ", "square entries exhibit more Z than circle entries", etc.
|
|
50
|
+
All buckets must make explicit references to both classes of entries ("square entries" and "circle entries"), and explicitly claim that one class has more of an attribute than the other.
|
|
51
|
+
Bucket definitions must clearly explain what it means for a square entry to have more of the attribute than a circle entry (or vice versa, depending on the bucket), grounded in examples from the raw terms.
|
|
52
|
+
{% endif %}
|
|
53
|
+
|
|
54
|
+
{% if voting %}
|
|
55
|
+
You are selecting the {{ bucket_count }} bucket terms by voting.
|
|
56
|
+
The only bucket terms you can choose from are from the following set of bucket candidates:
|
|
57
|
+
BEGIN BUCKET CANDIDATES
|
|
58
|
+
{{ bucket_candidates }}
|
|
59
|
+
END BUCKET CANDIDATES
|
|
60
|
+
|
|
61
|
+
From these candidates only, choose the buckets (verbatim as they appear in the candidate keys) that best group the raw terms with the desired specificity, substance, and mutual exclusivity.
|
|
62
|
+
{% if selected_buckets %}
|
|
63
|
+
The following bucket terms have **already been selected**; you should **not** select any similar/overlapping buckets from the candidate list:
|
|
64
|
+
|
|
65
|
+
BEGIN ALREADY FINALIZED BUCKETS
|
|
66
|
+
{{ selected_buckets }}
|
|
67
|
+
END ALREADY FINALIZED BUCKETS
|
|
68
|
+
|
|
69
|
+
You only need to vote for {{ bucket_count - (selected_buckets|length) }} additional buckets, not the full {{ bucket_count }}.
|
|
70
|
+
This is because the ultimate, final set of the best, mutually exclusive buckets will be the union of the already selected buckets and the additional buckets you vote for.
|
|
71
|
+
Thus, when voting for the {{ bucket_count - (selected_buckets|length) }} additional buckets, ensure they are mutually exclusive both with each other **and** with the already selected buckets above.
|
|
72
|
+
Consider the selected buckets to locked in; your new selections should not overlap with them at all.
|
|
73
|
+
Choose the {{ bucket_count - (selected_buckets|length) }} additional buckets from the candidate list such that the final set of all selected buckets (including the already selected ones) are mutually exclusive from each other and excellent at grouping as many raw terms as reasonable, without losing specificity and substance.
|
|
74
|
+
It is like being GM of an NBA team: you already have some players on the roster, and you need to pick new ones that fit well with the existing ones and make a great team overall.
|
|
75
|
+
These already finalized buckets are your existing players; now focus on identifying the best complementary pieces to add to the team to get the best overall coverage with the final set of buckets.
|
|
76
|
+
It is essential you avoid voting for terms that overlap with the already selected buckets.
|
|
77
|
+
DON'T vote for any bucket that is conceptually already covered by the already selected buckets.
|
|
78
|
+
Strongly prefer new buckets that are orthogonal to existing buckets, explaining a distinct but important pattern in the data.
|
|
79
|
+
If the obvious patterns are already covered, novelty (without triviality) is highly prized.
|
|
80
|
+
Build a full, mutually exclusive set of buckets that together capture the biggest themes present in the raw terms, amongst the totality of all existing and new buckets you are voting for.
|
|
81
|
+
|
|
82
|
+
Again, the already selected buckets are: {{ selected_buckets.keys() }}
|
|
83
|
+
You must vote for exactly {{ bucket_count - (selected_buckets|length) }} additional buckets.
|
|
84
|
+
{% else %}
|
|
85
|
+
You must vote for exactly {{ bucket_count }} buckets from the candidate list keys.
|
|
86
|
+
{% endif %}
|
|
87
|
+
Output JSON only, in following format:
|
|
88
|
+
{
|
|
89
|
+
"<insert first voted for candidate bucket here verbatim>": "<insert short 1 sentence justification on why this bucket and how it is mutually exclusive from all other selected buckets here>",
|
|
90
|
+
"<second voted for candidate bucket verbatim>": "...",
|
|
91
|
+
...
|
|
92
|
+
}
|
|
93
|
+
{% else %}
|
|
94
|
+
Each bucket definition/justification should concisely (1-3 sentences) explain what it means to fall into that bucket, also mentioning some member raw terms/direct evidence as illustrative examples.
|
|
95
|
+
It should be clear what it means for some relevant content to fall into each bucket or have more/less of it as an attribute.
|
|
96
|
+
Output JSON only, in following format:
|
|
97
|
+
{
|
|
98
|
+
"<insert bucket here>": "<insert efficient definition of bucket and justification with direct examples, evidence, and reasoning (1-3 sentences)>",
|
|
99
|
+
"<bucket>": "...",
|
|
100
|
+
...
|
|
101
|
+
}
|
|
102
|
+
Avoid using underscores in bucket terms; ensure easy readability (should understand specific concept just by reading the term).
|
|
103
|
+
{% endif %}
|
|
104
|
+
|
|
105
|
+
{% if additional_instructions %}
|
|
106
|
+
The user has provided the following additional instructions, clarifications, or labelled examples (if these provided, rely heavily on them to calibrate yourself).
|
|
107
|
+
If they conflict with the other instructions, these user instructions take precedence.
|
|
108
|
+
If they specify a specific scope to confine your buckets to, it is essential you comply with that scope, focus closely on it, and not output any buckets outside of it.
|
|
109
|
+
|
|
110
|
+
BEGIN ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
111
|
+
{{ additional_instructions }}
|
|
112
|
+
END ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
113
|
+
{% endif %}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{% import "snippets.jinja2" as snip %}
|
|
2
|
+
{% if differentiate %}
|
|
3
|
+
{{ snip.pair_entries(modality, entry_square, entry_circle, circle_first=circle_first|default(false)) }}
|
|
4
|
+
You are specifically looking at features that distinguish the two entries from each other.
|
|
5
|
+
When deciding which labels apply, assess whether the label captures a key difference between the two entries.
|
|
6
|
+
Pay close attention to the specific entry class, square or circle.
|
|
7
|
+
Both directions of the label (square over circle and circle over square) are provided on each attribute; only one can be True, the other must be False.
|
|
8
|
+
Default to both being False unless clear and evident that one applies to the provided entries. Often, neither will apply because the attribute is not present, or is equally present, in which case both must be False.
|
|
9
|
+
Again: for each label pair (same attribute, inverted directions), both can be False (default) or one can be True while other is False. Both cannot be True.
|
|
10
|
+
If one label in a pair concerns circle entries having more of an attribute than square entries, then that label applies/returns True only if the circle entry here has that attribute MORE than the square entry here.
|
|
11
|
+
Insufficient simply for attribute to be present; the specific condition that the provided circle entry manifests the attribute more than the provided square entry must be clearly and evidently true, for this label to return True.
|
|
12
|
+
If instead the square entry manifests the attribute more than the circle entry: then the "circle more than square" label of the pair must return False, while the "square more than circle" label of the pair would return True.
|
|
13
|
+
If the attribute is absent from both square and circle, or is equally present in both, then both paired labels must return the default False.
|
|
14
|
+
Triple check your logic and ensure you apply the correct label of each pair (True for one, False for the other), and default to False for both unless clear and evident that one of the pair is True.
|
|
15
|
+
Evaluate each pair separately from other pairs.
|
|
16
|
+
{% else %}
|
|
17
|
+
{{ snip.single_entry(modality, text) }}
|
|
18
|
+
{% endif %}
|
|
19
|
+
|
|
20
|
+
Task: For each label below, decide whether it applies to the provided content.
|
|
21
|
+
|
|
22
|
+
BEGIN LABELS
|
|
23
|
+
{{ attributes | shuffled_dict }}
|
|
24
|
+
END LABELS
|
|
25
|
+
Each key is a label. If a definition is provided, use it to anchor judgment; otherwise use your best consistent definition.
|
|
26
|
+
|
|
27
|
+
Rules (per label):
|
|
28
|
+
- Output True if label applies to the content; False if not
|
|
29
|
+
- Zero, one, or many labels may apply; mark True for all that apply
|
|
30
|
+
- Independently judge each label; no cross-label inference
|
|
31
|
+
|
|
32
|
+
Output JSON only, in following format:
|
|
33
|
+
{
|
|
34
|
+
"<insert label name here>": <insert True if this label applies, False if not>,
|
|
35
|
+
"<label name>": <insert False|True>,
|
|
36
|
+
...
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Labels you are evaluating are: {{ attributes.keys() | shuffled }}
|
|
40
|
+
Assess EVERY label (False or True); no drops. Use these label names verbatim, with absolutely no modification.
|
|
41
|
+
Same case, same spelling, same punctuation, same formatting.
|
|
42
|
+
|
|
43
|
+
{% if additional_instructions %}
|
|
44
|
+
The user has provided the following additional instructions, clarifications, or labelled examples (if these provided, rely heavily on them to calibrate your judgment).
|
|
45
|
+
If they conflict with the other instructions, these user instructions take precedence.
|
|
46
|
+
Important: if user has explicitly asked for only the best label, output True for ONLY the one label that best applies, and False for all others.
|
|
47
|
+
BEGIN ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
48
|
+
{{ additional_instructions }}
|
|
49
|
+
END ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
50
|
+
{% endif %}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{% import "snippets.jinja2" as snip %}
|
|
2
|
+
{% if entry_circle is defined %}
|
|
3
|
+
{{ snip.pair_entries(modality, entry_square, entry_circle, circle_first=circle_first|default(false)) }}
|
|
4
|
+
{% else %}
|
|
5
|
+
{{ snip.single_entry(modality, text) }}
|
|
6
|
+
{% endif %}
|
|
7
|
+
{% if modality != "text" %}
|
|
8
|
+
Since these inputs are not text, you should not output verbatim excerpts.
|
|
9
|
+
Instead, describe the relevant parts of the entity being coded, in your own words.
|
|
10
|
+
{% endif %}
|
|
11
|
+
|
|
12
|
+
Your task is to act as a passage coder, as is done in the social sciences.
|
|
13
|
+
|
|
14
|
+
{% if categories %}
|
|
15
|
+
You are to carefully read and dissect the text, marking the boundaries of snippets/subsections of the text that belong to the following categories/attributes:
|
|
16
|
+
|
|
17
|
+
BEGIN CATEGORIES/ATTRIBUTES
|
|
18
|
+
{{ categories }}
|
|
19
|
+
END CATEGORIES/ATTRIBUTES
|
|
20
|
+
Follow the specific definitions of the categories in assigning them to snippets/subsections of the text.
|
|
21
|
+
{% else %}
|
|
22
|
+
Consider the broad topic alluded to in these user instructions:
|
|
23
|
+
BEGIN USER INSTRUCTIONS
|
|
24
|
+
{{ additional_instructions }}
|
|
25
|
+
END USER INSTRUCTIONS
|
|
26
|
+
Consider the specific category labels that fall under the broad topic alluded to in the user instructions.
|
|
27
|
+
For example, if the broad topic is Bible verses, the specific category labels would be specific Bible verses like "John 3:16" or "Psalm 23:1".
|
|
28
|
+
Or if the topic is Supreme Court decisions, the specific category labels would be specific Supreme Court decisions like "Marbury v. Madison" or "Brown v. Board of Education".
|
|
29
|
+
In your output, you would only output as category labels the specific category labels (under the broad topic) that are present in the text and should be marked.
|
|
30
|
+
So, in the example above, if the text alludes to the Bible verse "John 3:16", you would output "John 3:16" as the category label and mark the snippets/subsections that allude to the John 3:16.
|
|
31
|
+
You would not output every single Bible verse; just every Bible verse that is present in the text.
|
|
32
|
+
Be thorough and exhaustive in your output, denoting every specific category label that is applicable in the text and marking every snippet/subsection that is applicable to each category label.
|
|
33
|
+
{% endif %}
|
|
34
|
+
|
|
35
|
+
You mark the boundaries of the snippets/subsections by verbatim outputting the first 8 words and the last 8 words of the snippet/subsection (more if necessary to ensure uniqueness in programmatically identifying the snippets/subsections).
|
|
36
|
+
If the snippet/subsection is less than 20 words, you should write out the entire snippet/subsection as the beginning excerpt and leave the ending excerpt blank.
|
|
37
|
+
It is crucial that these beginning and ending excerpts are completely verbatim, same exact spelling/mistakes/punctuation/capitalization/line breaks/etc.
|
|
38
|
+
We will programmatically use these excerpts to identify and pull out the whole snippets/subsections that you are marking.
|
|
39
|
+
Be sure that the beginning and ending excerpts are completely verbatim, same exact everything, same exact characters.
|
|
40
|
+
|
|
41
|
+
Output a JSON in the following format:
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
"<insert category name here>":
|
|
45
|
+
[
|
|
46
|
+
{
|
|
47
|
+
"beginning excerpt": "<insert the first 8 words of the snippet/subsection here verbatim (or the entire snippet/subsection if it is less than 20 words)>",
|
|
48
|
+
"ending excerpt": "<insert the last 8 words of the snippet/subsection here verbatim (or leave blank if the snippet/subsection is less than 20 words)>",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"beginning excerpt": "<insert the first 8 words of a different snippet/subsection here verbatim (or the entire snippet/subsection if it is less than 20 words)>",
|
|
52
|
+
"ending excerpt": "<insert the last 8 words of that snippet/subsection here verbatim (or leave blank if the snippet/subsection is less than 20 words)>",
|
|
53
|
+
},
|
|
54
|
+
...
|
|
55
|
+
]
|
|
56
|
+
"<category name>",
|
|
57
|
+
[
|
|
58
|
+
{
|
|
59
|
+
"beginning excerpt": "...",
|
|
60
|
+
"ending excerpt": "...",
|
|
61
|
+
},
|
|
62
|
+
...
|
|
63
|
+
],
|
|
64
|
+
...
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
{% if categories %}
|
|
68
|
+
Again: the categories you are marking are: {{ categories.keys() | shuffled }}
|
|
69
|
+
Ensure you input these category names verbatim into the JSON.
|
|
70
|
+
Exact spelling/capitalization/underscoring/any other formatting of the category names must be preserved.
|
|
71
|
+
Carefully consider the entire text for each and every category; do not skip any (though it is possible that some categories are not present in the text, in which case that category name must still be in the JSON but with an empty list of snippets/subsections).
|
|
72
|
+
{% endif %}
|
|
73
|
+
It is possible and often the case that text will overlap between categories, in which case you should mark the text for each applicable category separately. The same area of text can be marked for multiple categories.
|
|
74
|
+
Often in this case, it is not the exact same snippet/subsection that is being marked for each category, but rather a slightly different snippet/subsection that perhaps starts or ends at a different word, or is a different length.
|
|
75
|
+
You must carefully consider extremely precise boundaries for the snippets/subsections distinctly for each category.
|
|
76
|
+
Be precise with your identified snippets/subsections; don't include any words/chunks of text that are outside the essential boundaries of the snippet/subsection where the category applies.
|
|
77
|
+
Be very thorough. Every single applicable snippet/subsection (include small ones) throughout the text, beginning to end, must be marked.
|
|
78
|
+
This requires meticulosity and attention to detail.
|
|
79
|
+
Patiently find every single snippet/subsection that fits each category, and be willing to mark a large number of snippets/subsections if necessary.
|
|
80
|
+
Also be willing to mark the same or similar snippets/subsections under multiple categories separately if applicable.
|
|
81
|
+
Don't be lazy and mark giant chunks of text where most of it doesn't fit the category. In this case, only mark narrowly scoped excerpts of the text that fit the category.
|
|
82
|
+
Rigorously consider every single category. Remember that the same or similar snippets/subsections can be marked separately under multiple categories.
|
|
83
|
+
It is absolutely crucial that you look through the entire text. Do not skim through the middle and end; don't only mark snippets/subsections at the beginning. Full, thorough coverage is key.
|
|
84
|
+
Scour every single sentence, phrase, and paragraph of the text. Double check that you have not missed any snippets/subsections and that you have not neglected any portions of the text in your thinking and marking.
|
|
85
|
+
Make extra effort to check each of the beginning, middle, and end of the text to ensure you have not missed any snippets/subsections there. Cover everything, beginning to end.
|
|
86
|
+
It is crucial that you think through each portion of the text carefully and thoroughly, otherwise you will miss snippets/subsections.
|
|
87
|
+
|
|
88
|
+
{% if additional_instructions %}
|
|
89
|
+
These are additional instructions from the user, follow them carefully. They take precedence over the general instructions above, if there is a conflict.
|
|
90
|
+
In particular, consider any instructions, if provided, on the scope of the text to be marked (should you be marking very specific phrases or full paragraphs/sections?).
|
|
91
|
+
If the user indicates a different scope of how large the snippets/subsections should be/how much context to include, follow their instructions over the general instructions above, if there is a conflict.
|
|
92
|
+
BEGIN ADDITIONAL INSTRUCTIONS
|
|
93
|
+
{{ additional_instructions }}
|
|
94
|
+
END ADDITIONAL INSTRUCTIONS
|
|
95
|
+
{% endif %}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{% import "snippets.jinja2" as snip %}
|
|
2
|
+
{{ snip.pair_entries(modality, entry_square, entry_circle, circle_first=circle_first|default(false)) }}
|
|
3
|
+
|
|
4
|
+
{% if differentiate %}
|
|
5
|
+
Your task is to list concise, mutually exclusive terms or short phrases that best explain how entry "circle" differs from entry "square".
|
|
6
|
+
These terms should be specific and reflect clear differences observable between these particular entries.
|
|
7
|
+
There can be any number of terms from none up; do not invent terms if there is no meaningful difference.
|
|
8
|
+
Compile all relevant differences; be thorough.
|
|
9
|
+
|
|
10
|
+
When listing differences, explicitly note whether "square" or "circle" shows more or less of the attribute. For example, say "circle has more X than square" or "square lacks Y whereas circle has some".
|
|
11
|
+
Each extracted term should be a directional statement (e.g. "square exhibits strong communication amongst colleagues, circle does not", "circle uses lots of first person perspective whereas square rarely does", "square involves more chronic health issues than circle", "complications from type 2 diabetes seen in square but not circle", "circle has no direct quotes from evidence, square has some", etc) rather than an "X vs Y" phrase.
|
|
12
|
+
Strong preference for descriptive, highly specific, and directional terms-avoid vague and broad terms, and terms where it is unclear what it means for an entry to have more or less of the term.
|
|
13
|
+
Again: all about differences. An attribute shown in one entry but not the other is a difference. An attribute shown to a greater extent in one entry than the other is a difference.
|
|
14
|
+
|
|
15
|
+
Shortened example output:
|
|
16
|
+
{
|
|
17
|
+
"circle mentions renewable energy more than square": "Circle discusses wind and solar incentives extensively (stating 'A' and 'B'), whereas square only briefly references green jobs (focusing instead on 'C').",
|
|
18
|
+
"square uses historic evidence and anecdotes while circle does not": "Square highlights historical evidence and anecdotes in its argument (citing 'D', 'E'), whereas circle invokes no details from history.",
|
|
19
|
+
...
|
|
20
|
+
}
|
|
21
|
+
Stick closely to this "square focuses more on ... than circle", "circle advocates for ... more than square", "square uses ... while circle does not" general style in your term names.
|
|
22
|
+
{% else %}
|
|
23
|
+
Your task is to list concise, mutually exclusive terms or short phrases that best explain how entry "circle" is similar to entry "square".
|
|
24
|
+
These terms should be specific and reflect clear similarities observable between these particular entries.
|
|
25
|
+
There can be any number of terms from none up; do not invent terms if there is no meaningful similarity.
|
|
26
|
+
Compile all relevant similarities; be thorough.
|
|
27
|
+
|
|
28
|
+
Shortened example output:
|
|
29
|
+
{
|
|
30
|
+
"focus on renewable energy": "Both entries discuss wind and solar incentives extensively. Entry circle says 'A' and 'B', and entry square shows a similar focus, stating 'C' and 'D'.",
|
|
31
|
+
"use of historic evidence and anecdotes": "Both passages highlight historical evidence and anecdotes in their arguments. Entry square cites 'E'; entry circle invokes 'F' and 'G'.",
|
|
32
|
+
...
|
|
33
|
+
}
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
Don't be vague or generic. Be precise and parsimonious.
|
|
37
|
+
Each term must have a brief justification, directly quoting/using evidence from the entries where relevant.
|
|
38
|
+
No terms, one term, three, six, many terms may apply-don't skip any applicable terms but output zero/few if that is the correct choice.
|
|
39
|
+
Avoid trivial and pedantic terms; only substantive, meaningful terms-zero/fewer terms better than pointless trivialities.
|
|
40
|
+
Ensure your terms are mutually exclusive; each should be a distinct idea and must not overlap with other terms.
|
|
41
|
+
Include both obvious and subtle but relevant observations-think broadly and approach the comparison from different angles.
|
|
42
|
+
Employ creative thinking to assess from many different angles so that you notice non-obvious (but still relevant) points of comparison too.
|
|
43
|
+
ABSOLUTELY no underscores/hyphens in your term names-just natural, parsimonious language, like the examples.
|
|
44
|
+
|
|
45
|
+
Output JSON only, mapping each term to a 1-3 sentence justification:
|
|
46
|
+
{
|
|
47
|
+
"<insert term name here>": "<insert direct quotes/evidence and reasoning as justification (1-3 evidence dense sentences)>",
|
|
48
|
+
"<term>": "...",
|
|
49
|
+
...
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
{% if additional_instructions %}
|
|
53
|
+
The user has provided the following additional instructions, clarifications, or labelled examples (if these provided, rely heavily on them to calibrate yourself).
|
|
54
|
+
If they conflict with the other instructions, these user instructions take precedence.
|
|
55
|
+
If they specify a specific scope to confine your terms to, it is essential you comply with that scope, focus closely on it, and not output any terms outside of it.
|
|
56
|
+
|
|
57
|
+
BEGIN ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
58
|
+
{{ additional_instructions }}
|
|
59
|
+
END ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
60
|
+
{% endif %}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Consider the following raw terms:
|
|
2
|
+
BEGIN RAW TERMS
|
|
3
|
+
{{ raw_terms }}
|
|
4
|
+
END RAW TERMS
|
|
5
|
+
{% if modality == "text" %}
|
|
6
|
+
Each raw term is a text snippet. You are deduplicating the texts. In your output, use the identifier keys for each text to denote all representative / mapped texts. Only use those identifier keys in the mapping, not the full texts.
|
|
7
|
+
{% endif %}
|
|
8
|
+
|
|
9
|
+
Deduplicate these raw terms.
|
|
10
|
+
Do this by exhaustively identifying all subsets of raw terms such that all terms in the subset are largely equivalent to each other.
|
|
11
|
+
For each subset, map the raw terms to a single representative term.
|
|
12
|
+
The representative term must be one of the raw terms in the subset - it should be the best, most specific representative of the subset.
|
|
13
|
+
Strongly prefer precise and detailed representative terms; avoid vague, overly broad, or overly abstract representative terms (vague terms are better as simple members of the subset, not representatives).
|
|
14
|
+
Important: all raw terms that are not duplicates should be left alone and not included in output.
|
|
15
|
+
Only output the deduplication map, exhaustively compiling all subsets of largely equivalent raw terms and ignoring the rest.
|
|
16
|
+
|
|
17
|
+
All terms in the map must be taken from the raw terms verbatim, absolutely no modification.
|
|
18
|
+
Same case, same spelling, same punctuation, same formatting.
|
|
19
|
+
|
|
20
|
+
Output JSON only, in following format:
|
|
21
|
+
{
|
|
22
|
+
"<insert representative term here>": ["<insert raw term in corresponding subset here>", "<raw term>", ...],
|
|
23
|
+
"<representative term>": ["<raw term>", ...],
|
|
24
|
+
...
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
Again: representative term is generally not the broadest catch-all raw term; it is specific, precise, detailed - the term that all other raw terms in the subset are largely equivalent to.
|
|
28
|
+
Include the representative term twice: both as the key AND as member in the list of raw terms in subset.
|
|
29
|
+
Don't force meaningfully distinct terms into the same subset; it is fine if most or all terms are ignored and not deduplicated because they have no duplicate(s).
|
|
30
|
+
If no duplicates present, output an empty JSON.
|
|
31
|
+
Nonetheless, be thorough and exhaustive on all genuine duplicates - even pairs of just two equivalent terms must be deduplicated.
|
|
32
|
+
There may be many such pairs/triples/etc and you must find all that exist.
|
|
33
|
+
|
|
34
|
+
{% if additional_instructions %}
|
|
35
|
+
The user has provided the following additional instructions, clarifications, or labelled examples (if these provided, rely heavily on them to calibrate yourself).
|
|
36
|
+
If they conflict with the other instructions, these user instructions take precedence.
|
|
37
|
+
|
|
38
|
+
BEGIN ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
39
|
+
{{ additional_instructions }}
|
|
40
|
+
END ADDITIONAL INSTRUCTIONS/CLARIFICATIONS
|
|
41
|
+
{% endif %}
|