This repository shares tools developed by Jigsaw as a proof of concept to help make sense of large-scale online conversations. It demonstrates how Large Language Models (LLMs) like Gemini can be leveraged for such tasks. The code provided here offers a transparent look into Jigsaw's methods for categorization, summarization, and identifying points of agreement and disagreement in free response public opinion research. Our goal in sharing this is to inspire others by providing a potential starting point and useful elements for those tackling similar challenges.
To learn more about Jigsaw’s previous work in Sensemaking, please also visit our case studies repository.
Effectively understanding large-scale public input is a significant challenge. Traditional methods require choosing between the breadth of polls or the depth of focus groups. This initiative showcases how Google's Gemini models can allow those seeking to understand public opinion to get the best of both approaches, transforming massive volumes of raw community feedback into clear, digestible insights.
The tools in the src directory are provided as a functional pipeline you can run on your own data. You can leverage these components to transform raw community feedback into structured insights. Specifically, tools are provided for:
- Adaptive Interviewing: Launch surveys with Adaptive Interviewing to gather dynamic, follow-up driven responses.
- Data Preparation: Reformat and moderate raw exports from Qualtrics into a consistent structure for analysis.
- Topic Modeling and Quote Extraction: Use Gemini to identify discussion topics and extract representative quotes from participant dialogue.
- Quote Ranking: Apply classifiers to score quotes on attributes like reasoning and curiosity, helping you identify high-quality contributions.
- Discussion Summarization: Automatically generate report text, including overviews and topic summaries.
- Interactive Visualization: Build a user-friendly web interface for exploring the synthesized data.
- Proposition Generation & Simulated Juries: Generate and rank propositions using a Simulated Jury technique to identify consensus and create nuanced summaries.
This guide provides step-by-step instructions for running the Sensemaking tools. This pipeline transforms raw survey data into structured insights, including summaries, and visualizations.
Before you begin, ensure you have the following installed:
- Python 3 and pip3
- Node.js and NPM (required for the interactive visualization step)
- A Gemini API Key (and optionally a Google Cloud API Key for DLP)
Install the Python dependencies by running:
pip3 install -r requirements.txtConfirm all tests pass by running:
python3 -m pytest src/If you are using Qualtrics for data collection, you can use the following script to transform your raw survey export into a standard format. You will need to specify which columns map to your fixed questions and AI follow-up questions.
bash src/survey_processing.sh \
--input_csv <PATH_TO_RAW_EXPORT.csv> \
--output_dir <OUTPUT_DIR> \
--round_1_question_response_text "Q1,Q35,Q36" \
--round_1_follow_up_questions "Q1FU,Q2FU,Q3FU" \
--round_1_follow_up_question_response_text "Q23,Q37,Q39"This generates a processed.csv file in your output directory.
The pipeline can be run on data from sources other than Qualtrics. Later steps only require a CSV with columns for participant_id (a unique identifier for each participant) and survey_text (the content to be analyzed).
Working with non-English or multilingual responses? See Working with Non-English and Multilingual Data below for guidance and considerations.
(Optional) Run Moderation and Quality Checks: Utilities are provided to score the responses for quality and flag rows that may need moderation by a human reviewer. You can get these scores by running:
bash src/moderation.sh \
--processed_csv <OUTPUT_DIR>/processed.csv \
--output_dir <OUTPUT_DIR> \
--gemini_api_key "$GEMINI_API_KEY" \
--gcloud_api_key "$GCLOUD_API_KEY"Use Gemini to discover discussion topics and extract representative quotes from the dialogue. This script can be run on data from sources other than qualtrics. It only requires an input csv with columns for participant_id (a unique identifier for each participant) and survey_text (the text to be analyzed).
python3 -m src.categorization_runner \
--input_file <OUTPUT_DIR>/processed.csv \
--output_dir <OUTPUT_DIR>/categorization \
--gemini_api_key "$GEMINI_API_KEY" \
--additional_context_file src/default-additional-context.md-
-
--skip_quote_extraction: Skips extracting specific quotes and uses the full participant response instead. -
--topics "Topic A,Topic B": Provide a list of predefined topics instead of letting the model discover them. -
--max_concurrent_calls N(or-c N): Maximum number of concurrent LLM API calls, applied to every stage of categorization. Defaults to 100 for Gemini and 20 for open models. Must be at least 1.
In addition to Google's Gemini API, the pipeline supports running on open models (such as Gemma) or any other open-weights model served via an OpenAI API compatible endpoint (e.g., using vLLM, LiteLLM, Ollama, or LM Studio).
To route requests to an open model instead of Gemini, configure the following environment variables before running the pipeline:
# 1. Instruct the factory to use the OpenAI API compatible client
export MODEL_ENDPOINT_TYPE="openai_api_compatible"
# 2. Set the URL of your inference server (e.g., vLLM or LM Studio)
export OPENAI_API_ENDPOINT_URL="http://localhost:8000/v1"
# 3. Set the API key (use a dummy value if your local server doesn't require one)
export OPENAI_API_ENDPOINT_KEY="your-api-key-or-dummy"
# (Optional) Choose the response format style
# - "openai_json_schema": Uses the modern structured JSON schema format (default, recommended for vLLM/LM Studio)
# - "llama_cpp_old": Fallback for older llama-cpp servers
export OPENAI_RESPONSE_FORMAT_STYLE="openai_json_schema"Then, run the runner passing the path or identifier of the model to the --model_name flag:
python3 -m src.categorization_runner \
--input_file <OUTPUT_DIR>/processed.csv \
--output_dir <OUTPUT_DIR>/categorization \
--additional_context_file src/default-additional-context.md \
--model_name "google/gemma-4-26b-it" # Or the absolute path to your local model weightsSystem One decision models answer typed questions (yes/no, a choice among options, or a position on an ordered scale) with probabilities, in one pass, instead of generating text. They do not implement chat, so MODEL_ENDPOINT_TYPE=openai_api_compatible cannot point at them. When the variables below are set, judgment stages send their questions to a decision model. Generative stages stay on Gemini or the OpenAI-compatible model: topic and opinion names, quote extraction, report text, proposition writing, translation, and jury ranking.
Judgment stages that move: topic assignment, opinion assignment, moderation and bridging scores, opinion autorater verdicts, other eval verdicts, jury approval votes, proposition equivalence clustering, and the dedup winner / topic-home pick.
export DECISION_ENDPOINT_TYPE="systemone"
export DECISION_MODEL="clef"With only these two set, requests go to POST /v1/systemone on a local Ollama 0.35.1+ server running Clef. Optional settings, which only need to be set to change the default:
DECISION_MODEL_HOST: where the model is served. A bare host such asgpu-boxorgpu-box:11434is read the way the Ollama CLI readsOLLAMA_HOST. A URL without a path gets/v1/systemone. A URL with a path is used as given.DECISION_MODEL_API_KEY: bearer key for a hosted endpoint.DECISION_MODEL_KEEP_ALIVE: how long Ollama keeps the model loaded between stages (default30mfor the default local server). OnceDECISION_MODEL_HOSTis set it is sent only when given, because other servers reject the field.DECISION_MAX_QUESTIONS: most questions per request (default64, Ollama's limit). Lower it for hosts with a smaller limit.DECISION_THRESHOLD: probability cutoff for labels and equivalence, between 0 and 1 (default0.5).DECISION_MAX_CONCURRENT: requests in flight (default1, because a local runner has one slot).
Locally, Clef and pplx-decider:27b-q4_K_M have been tested on Ollama 0.40.11. Hosted endpoints that have been tested with the same request shape:
| Model | DECISION_MODEL_HOST |
DECISION_MODEL |
|---|---|---|
| Clef on Cloudflare Workers AI | https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/run/@cf/cloudflare/clef |
clef |
| TypeSafe Jev | https://api.typesafe.ai |
jev-latest |
| Inception Mercury Decide | https://api.inceptionlabs.ai/v1/decisions |
mercury-decide |
Assignment asks one yes/no question per label, and Other is not asked: it is assigned only when no other label clears the threshold. Labels beyond the question limit are split across requests. Equivalence is pairwise, then joined in code. Approval votes use the existing scale. Autorater verdicts are a score question on the 0-4 rubric, read at its most likely level, with no written explanation; a level below autorater_min_score is finalized as Other without retry, because the decision is deterministic. Ranking still needs a generative model.
Apply classifiers to score the extracted quotes on attributes like reasoning, personal stories, and curiosity. This helps surface the most constructive contributions. By default these are the first quotes shown in the interactive report.
python3 -m src.get_bridging_scores \
--input_csv <OUTPUT_DIR>/categorization/categorized_without_other_filtered.csv \
--output_csv <OUTPUT_DIR>/bridging_scores.csv \
--gemini_api_key "$GEMINI_API_KEY"To save quota, the script only scores what it needs to:
- Rows that already have all three bridging scores are reused (for example, when re-running on a previous output). Rows with any score missing are scored in full. Pass
--force_rerunto rescore every row. - Duplicate quotes are scored once.
- Empty quotes are never scored. Optionally, pass
--min_text_length <N>to also skip quotes shorter thanNcharacters (default0, off).
Skipped rows have empty scores and a reason in the BRIDGING_SKIP_REASON column (empty_text, too_short, or scoring_failed).
Automatically generate topic-level summaries along with a summary of top-level take aways from the conversation.
python3 -m src.generate_report_text.generate_report_text \
--input_csv <OUTPUT_DIR>/bridging_scores.csv \
--output_dir <OUTPUT_DIR>/report_text \
--additional_context src/default-additional-context.mdThis script generates two JSON files in your output directory:
report_data.json: The primary file used by the interactive visualization. It contains the high-level overview and the summaries for each discovered topic.report_data_with_opinions.json: A more detailed version that also includes summaries for every individual opinion within the topics. This is useful for debugging or if you want to build a more granular custom interface.
The script follows a recursive summarization process to ensure accuracy:
- It summarizes the quotes for each opinion.
- It synthesizes those opinion summaries into a summary for the overall topic.
- It creates the high-level overview based on the topic summaries.
Generate distinct propositions and use a simulated jury technique to rank them and identify statements likely to receive broad agreement. These statements can be used in future validation polls.
Generate Propositions:
python3 -m src.propositions.proposition_generator \
--r1_input_file <OUTPUT_DIR>/categorization/categorized_without_other_filtered.csv \
--output_dir <OUTPUT_DIR>/propositions \
--gemini_api_key "$GEMINI_API_KEY"Rank and Refine with Simulated Jury:
python3 -m src.proposition_refinement.main \
--input_pkl <OUTPUT_DIR>/propositions/world_model.pkl \
--output_pkl <OUTPUT_DIR>/propositions/refined_world_model.pkl \
--gemini_api_key "$GEMINI_API_KEY" \
--run_pav_selectionExtract Final CSVs:
# Get all ranked propositions
python3 -m src.world_model.main --query=all_by_topic --output_format=csv \
<OUTPUT_DIR>/propositions/refined_world_model.pkl > final_propositions.csv
# Get all ranked nuanced propositions:
python -m src.world_model.main --query=all_nuanced --output_format=csv \ <OUTPUT_DIR>/propositions/refined_world_model.pkl > final_nuanced_propositions.csv
Optional: to simplify the language used in propositions (e.g. for nuanced propositions):
python -m src.proposition_simplification_runner \
--input_csv <INPUT_CSV> --output_csv <OUTPUT_CSV> \
--gemini_api_key "$GEMINI_API_KEY" --model_name gemini-3.5-flashINPUT_CSV should contain a single column called "original" with the original proposition text. The OUTPUT_CSV contains an additional column called "simplification" with the rewritten proposition.
Note: Standalone Simulated Juries
If you have a simple CSV of statements and participants and want to predict agreement without running the full pipeline, you can run the jury standalone:
python3 -m src.simulated_jury.main \
--participants_csv <CSV_WITH_PARTICIPANT_RESPONSES> \
--statements_csv <CSV_WITH_STATEMENTS_TO_TEST> \
--output_csv <OUTPUT_DIR>/jury_results.csv \
--statement_column "proposition"In order to build the web interface to explore the summarized data, begin by copying the processed data into the UI input folder.
cp <OUTPUT_DIR>/bridging_scores.csv src/report_ui/input/opinions.csv
cp <OUTPUT_DIR>/report_text/report_data.json src/report_ui/input/summary.jsonAlternatively, skip the copy step and pass paths explicitly:
cd src/report_ui
node build.js inline \
--bridging_scores <OUTPUT_DIR>/bridging_scores.csv \
--summary <OUTPUT_DIR>/report_text/report_data.json \
--output <OUTPUT_DIR>/report.htmlYou can edit src/report_ui/input/config.json to customize the report. Key options include:
title: Set the display title of the report.logo: Set the filename of your header image (placed in theinput/folder).overview_chart: Set the display mode for the main chart ("toggle","topics", or"opinions").number_of_sample_quotes: Control how many quote previews to display for each opinion.chart_colors: Provide an array of hex color codes to customize the chart palette.demographic_colors: Provide a single hex color code (to generate a six-step light-to-dark palette) or an array of color codes to customize the participant chart palette.excluded_topics: Add topic names to this array to hide them from the report.excluded_opinions: Add opinion names to this array to hide them from the report.- For a full list of configuration options, check the
README.mdfile insrc/report_ui/.
You can enhance your report by configuring two additional features before building:
- Demographic Support: You can add demographic dimensions to your data by ensuring the relevant columns in your
opinions.csvare prefixed withdemo:. For example,demo:Ageordemo:Location. The tool will automatically use these to build a demographic breakdown chart and quote filters. - Predicted Agreement (Propositions): Because you generated propositions in step 5, you can feature them in a dedicated "Predicted Agreement" tab. Copy your
selected_propositions.csvfile tosrc/report_ui/input/propositions.csv(it requires columns fortopic,proposition, andapproval_rate).
- Web Server Deployment (Static): Run
npm run buildfrom thesrc/report_uidirectory to output a version optimized for delivery via a web server tosrc/report_ui/output/static. - Offline Viewing (Inline): Run
npm run inlineto output a self-contained version of the report tosrc/report_ui/output/inline.- Note: This may not be suitable for larger conversations.
(Optional) Local Development: If you want to view the report using a live local server during development, you will need to install the dev dependencies first:
cd src/report_ui
npm install
npm run devAll system prompts and few-shot examples in the Sensemaking pipeline are written in English. While the pipeline includes utilities for analyzing non-English or mixed-language conversations and requesting outputs in a specific target language, multilingual performance can vary significantly depending on both the underlying data, including both the size of the dataset and the languages involved, and the model used:
- Model Choice: Frontier multilingual models (such as Gemini) can often categorize mixed-language inputs and follow target-language output instructions while still adhering to structured JSON schemas. Smaller models or open-weights models connected via the OpenAI-compatible endpoint may struggle with cross-lingual clustering, complex multi-step reasoning (such as Simulated Juries), or maintaining non-English output alongside English instructions.
- Language Resource Level: Even with frontier models, lower-resource languages typically have higher token density, slower processing times, and less reliable instruction-following than high-resource languages.
- Validation Recommended: If you are working with non-English data or non-English output targets, we recommend testing your chosen model on a small sample first to decide whether to run natively on the original text or pre-translate your dataset.
By default, when no output language is specified:
- Mixed-language datasets and lower-resource languages typically produce English outputs (topics, opinions, summaries, and propositions).
- Monolingual datasets in high-resource languages (such as Spanish) often infer the input language for topic and opinion discovery, but can still unpredictably drift back into English.
As a result, explicitly setting a target output language is generally recommended when working with multilingual and non-English data. To explicitly instruct the model to generate all synthesized outputs (topics, opinions, summaries, propositions, and simplified propositions) in a specific language, set OUTPUT_LANGUAGE in your environment (or .env file) or pass --output_language to each runner step:
# Option 1: Set once for all pipeline steps
export OUTPUT_LANGUAGE="Spanish"
# Option 2: Pass --output_language to individual LLM runner commands
python3 -m src.categorization_runner \
--input_file <OUTPUT_DIR>/processed.csv \
--output_dir <OUTPUT_DIR>/categorization \
--gemini_api_key "$GEMINI_API_KEY" \
--additional_context_file src/default-additional-context.md \
--output_language "Spanish"Note: --output_language only affects model-generated labels, summaries, and propositions. Extracted participant quotes remain verbatim in the participant's original language unless translated separately.
Sensemaking can create reports from participant responses in multiple languages. There are 2 options for working with multilingual input:
- Default: Frontier multilingual models like Gemini can natively analyze mixed-language datasets (for example, English and Spanish responses in the same survey) without pre-translation—discovering shared topics, categorizing opinions, and generating summaries while preserving participants' original phrasing and tone.
- Pre-translation: We also provide an optional utility (
src.translate_csv) that translates participant input into a single language before analysis so downstream tasks only process one language.
Using src.translate_csv can be helpful in a number of scenarios, including:
- Low-Resource Languages or Smaller Open Models (Pre-Analysis): If your dataset includes low-resource languages, or if you are running the pipeline with smaller open-weights models that struggle with cross-lingual clustering and multi-step reasoning (such as Simulated Juries), translating
survey_textinto a single target language before Step 2 can improve consistency. - High Token-per-Word Languages: Because participant responses are passed to the model across many stages of the pipeline (topic discovery, categorization, autoraters, quote extraction, bridging scoring, summarization, and simulated juries), languages or scripts with significantly higher token counts per word can multiply token usage and costs. Translating once upfront into a more token-compact language can reduce overall token consumption.
- Translating Extracted Quotes for a Monolingual Audience (Post-Analysis): If you would like to translate all quotes in the final report to a single language, you can use the
translate_csv.pytool to translate thequotecolumn before generating the Interactive Report in Step 6.
The script detects the language of each entry and translates only rows that are not already in --target_language (default en_us). Whenever a column is translated, the original untranslated text is automatically preserved in a new original_text_<column> column (for example, original_text_survey_text or original_text_quote).
To pre-translate raw survey responses before analysis:
When run on survey_text (the default for --columns), the script automatically translates the inner <response>...</response> text from Qualtrics exports without altering <question> tags or XML structure, as well as plain-text survey_text columns from custom CSVs:
python3 -m src.translate_csv \
--input_csv <OUTPUT_DIR>/processed.csv \
--output_csv <OUTPUT_DIR>/processed_translated.csv \
--columns "survey_text" \
--target_language "en_us" \
--gemini_api_key "$GEMINI_API_KEY"To translate only extracted quotes at the end of the pipeline (before Step 6):
python3 -m src.translate_csv \
--input_csv <OUTPUT_DIR>/bridging_scores.csv \
--output_csv <OUTPUT_DIR>/bridging_scores_translated.csv \
--columns "quote" \
--target_language "en_us" \
--gemini_api_key "$GEMINI_API_KEY"--columns: Comma-separated list of columns to translate (defaults tosurvey_text).--target_language: Target language code or name to translate into (defaults toen_us).--model_name: Model used for language detection and translation (defaults togemini-3.8-flash).--gemini_api_key: Gemini API key (if omitted, falls back to theGEMINI_API_KEYenvironment variable).
The tools here show how Jigsaw is approaching the application of AI to the emerging field of “sensemaking.” It is offered as an insight into our experimental methods. While parts of this library may be adaptable for other projects, developers should anticipate their own work for implementation, customization, and ongoing support for their specific use case.
This repository offers a transparent view of Jigsaw's approach to large-scale conversation sensemaking with AI. Developers can:
- Review the code to understand Jigsaw's techniques with LLMs.
- Leverage components for their own projects (some customization may be needed).
- Use the command and prompt examples, and overall approach, as inspiration for their own sensemaking tools.
This codebase is not actively maintained, but we hope it can serve as inspiration for other projects and encourage experimentation and building upon the ideas shared here!