PsychScanner Parser Tutorial¶
Walks through every parser mode supported by psychscanner on the ps-parser branch:
| Mode | parser= value |
What it does |
|---|---|---|
| 1 | "0" or None |
No structured parsing — raw AIMessage content |
| 2 | "1" |
Read parser class name from task JSON, resolve via registry |
| 3 | "ClassName" |
String name — direct registry lookup |
| 4 | MyPydanticClass |
Pass a Pydantic class directly (recommended) |
| 5 | callable |
Per-trial dispatch function — replaces old "dynamic" |
Plus the cross-cutting flags parser_raw and parser_config.
ps-parser change:
parser='dynamic'is removed. Use a callable (Mode 5) or per-trial"parser"fields in the task JSON instead.
Backend requirement¶
Modes 2–5 call LangChain's with_structured_output(), which needs a real LLM provider
(the bundled mock-llm does not support structured output). This notebook uses a local
Ollama model — install Ollama and pull a small instruct model first:
ollama pull smollm2:360m-instruct-fp16
If you prefer OpenAI/Anthropic/Groq, swap MODEL_FAMILY and MODEL_NAME in the setup cell.
1. Setup¶
import json
import shutil
from pathlib import Path
from pprint import pprint
import psychscanner as psy
from psychscanner import ExpCard, ExpCardInit, ScannerModel
# Backend — change these two lines if you don't use Ollama
MODEL_FAMILY = "ollama"
MODEL_NAME = 'smollm2:360m-instruct-fp16'
PROJECT_DIR = Path.cwd() / "_parser_tutorial_runs"
if PROJECT_DIR.exists():
shutil.rmtree(PROJECT_DIR)
PROJECT_DIR.mkdir()
print(f"Outputs will land in: {PROJECT_DIR}")
/opt/anaconda3/envs/psyscan/lib/python3.11/site-packages/langgraph/checkpoint/base/__init__.py:17: LangChainPendingDeprecationWarning: The default value of `allowed_objects` will change in a future version. Pass an explicit value (e.g., allowed_objects='messages' or allowed_objects='core') to suppress this warning. from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
Outputs will land in: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
2. What parsers ship with the package?¶
The psychscanner.parsers top-level module exposes a unified registry of all bundled
Pydantic parser classes. Behind the scenes they live in two themed sub-modules:
psychscanner.datasets.prompts.parser_tasks— vividness + reality-monitoring (RM) parserspsychscanner.datasets.prompts.parser_general— Likert agreement, generic response/rating, word-class
The old module names (parser, parser_extra) still work as backward-compat shims.
Recommended way to discover parsers:
from psychscanner.parsers import list_parsers, get_parser
all_names = list_parsers() # sorted list from the registry
print(f"{len(all_names)} parsers registered:")
for name in all_names:
print(f" {name}")
41 parsers registered: AllResponseRMEI AllResponseRMEIN AllResponseRMIE AllResponseRMIEN Confidence16 DefaultLiteralAgree DefaultLiteralVivid010 DefaultLiteralVivid15 DefaultLiteralVivid15Pol DefaultParser DefaultRMEncodingPhase DefaultRatingParser DefaultResponseRating DefaultResponseRatingConvo DefaultRmChoiceConf16 DefaultWordCaseNonWord DefaultWordCaseNonWordConf16 JudgmentEI JudgmentEIN JudgmentIE JudgmentIEN Ready RelatednessRating ResponseRmScSt ResponseRmStEI ResponseRmStEIN ResponseRmStIE ResponseRmStIEN Response_part_1_rm Response_part_2_rm Response_part_2_rmrevo SimpleResponseRating Source TaskReadyConfidence TaskResponse Task_1_ResponseRate Task_2_ResponseRate Task_3_ResponseRate TwoResponses Word2 WordNonWord
Inspect a single parser to see what fields it expects:
# get_parser() looks up by name and raises a helpful KeyError on typos
cls = get_parser("DefaultLiteralAgree")
print("Class:", cls)
print()
print("Schema:")
print(json.dumps(cls.model_json_schema(), indent=2))
Class: <class 'psychscanner.datasets.prompts.parser_general.DefaultLiteralAgree'>
Schema:
{
"description": "Give response on a Likert Scale of range 1 to 5 for the given item.",
"properties": {
"rating": {
"description": "Agreement Rating.\n Rate agreement on the scale of 1 to 5, where:\n '5' to indicate that you absolutely agree that the statement describes you;\n '1' to indicate that you totally disagree with the statement\n '3' if you not sure, but always to make a choice.\n Always give a single integer rating value ranging from 1 to 5 on the given item.",
"enum": [
1,
2,
3,
4,
5
],
"title": "Rating",
"type": "integer"
}
},
"required": [
"rating"
],
"title": "DefaultLiteralAgree",
"type": "object"
}
3. Mode 1 — parser="0" (no parsing)¶
The LLM response passes through unchanged. with_structured_output() is not called, so this mode works with any model — including the bundled mock-llm. We'll use that here so the cell runs without Ollama.
task_file = Path.cwd() / "tasks" / "example_survey.json"
card_no_parser = ExpCardInit()
card_no_parser.proj_dir = PROJECT_DIR
card_no_parser.projectname = "mode1_no_parser"
card_no_parser.model = "mock-chat-model"
card_no_parser.family = "mock-llm"
card_no_parser.task_file = task_file
card_no_parser.parser = "0" # <-- no parser
card_no_parser.cogtype = "no"
card_no_parser.nsim = 1
card_no_parser.tunnel_status = "0"
exp = ExpCard(card_no_parser)
scanner = ScannerModel(expcard=exp)
results_mode1 = scanner.run()
print("\n--- First trial response (raw, no parsing) ---")
first_trial = results_mode1[0][0]
print("pred_resp:", first_trial["pred_resp"])
print("type: ", type(first_trial["pred_resp"]).__name__)
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/mode1_no_parser/example_survey/mock-llm_mock-chat-model_SingleTurn
----<>----
--<chat model>-- model_name='mock-chat-model' repeat_buffer_length=10
2026-05-07 11:24:23.888 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
4it [00:00, 37.92it/s]
6it [00:00, 50.22it/s]
2026-05-07 11:24:24.192 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:24:24.197 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
--- First trial response (raw, no parsing) ---
pred_resp: content='{\n "TRI' additional_kwargs={} response_metadata={'time_in_seconds': 3, 'model_name': 'mock-chat-model'} id='lc_run--019e030a-40be-7b81-8193-6455e3c2f880-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 333, 'output_tokens': 10, 'total_tokens': 343}
type: AIMessage
Notice the response is an AIMessage whose .content is the raw model output. No JSON, no schema validation.
4. Mode 2 — parser="1" (resolve from task JSON)¶
The task JSON has a "parser" key holding the class name as a string.
Setting parser="1" tells ExpCard to read that name and resolve it via
get_parser() — the registry lookup introduced in ps-parser (replacing the old
unsafe eval() approach).
Our tasks/example_survey.json declares "parser": "DefaultLiteralAgree".
card_from_json = ExpCardInit()
card_from_json.proj_dir = PROJECT_DIR
card_from_json.projectname = "mode2_from_json"
card_from_json.model = MODEL_NAME
card_from_json.family = MODEL_FAMILY
card_from_json.parameters = {"temperature": 0}
card_from_json.task_file = task_file
card_from_json.parser = "1" # <-- read parser name from task JSON
card_from_json.cogtype = "no"
card_from_json.nsim = 1
card_from_json.tunnel_status = "0"
exp = ExpCard(card_from_json)
print("Parser resolved to:", exp.parser.__name__)
scanner = ScannerModel(expcard=exp)
results_mode2 = scanner.run()
print("\n--- First trial response (parsed) ---")
pprint(results_mode2[0][0]["pred_resp"])
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/mode2_from_json/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
Parser resolved to: DefaultLiteralAgree --<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:24:26.314 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:09, 9.36s/it]
2it [01:29, 50.77s/it]
3it [02:09, 45.90s/it]
4it [02:49, 43.60s/it]
5it [04:32, 65.24s/it]
6it [05:49, 69.25s/it]
6it [05:49, 58.33s/it]
2026-05-07 11:30:16.559 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:30:16.627 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
--- First trial response (parsed) ---
AIMessage(content="{'rating': 5}", additional_kwargs={}, response_metadata={}, id='867e5425-cbc4-45a4-a908-c4a8f534a71c', tool_calls=[], invalid_tool_calls=[])
The response should now be a structured dict with a rating field constrained to {1, 2, 3, 4, 5}.
5. Mode 3 — pass a Pydantic class directly¶
Skip the JSON-name-and-eval round trip. This is the most explicit mode and the one to prefer when working in code.
from psychscanner.datasets.prompts.parser import DefaultLiteralVivid15
card_direct = ExpCardInit()
card_direct.proj_dir = PROJECT_DIR
card_direct.projectname = "mode3_direct_class"
card_direct.model = MODEL_NAME
card_direct.family = MODEL_FAMILY
card_direct.parameters = {"temperature": 0}
card_direct.task_file = task_file
card_direct.parser = DefaultLiteralVivid15 # <-- Pydantic class object
card_direct.cogtype = "no"
card_direct.nsim = 1
card_direct.tunnel_status = "0"
exp = ExpCard(card_direct)
scanner = ScannerModel(expcard=exp)
results_mode3 = scanner.run()
print("--- First trial response ---")
pprint(results_mode3[0][0]["pred_resp"])
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/mode3_direct_class/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:30:18.020 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [02:25, 145.81s/it]
2it [05:25, 165.76s/it]
3it [07:52, 157.08s/it]
4it [08:35, 112.32s/it]
5it [08:45, 75.09s/it]
6it [08:51, 51.91s/it]
6it [08:51, 88.66s/it]
2026-05-07 11:39:10.375 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:39:10.400 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
--- First trial response ---
AIMessage(content="{'Vividness': 1}", additional_kwargs={}, response_metadata={}, id='1d3943c7-c831-4029-a4ef-27179096e189', tool_calls=[], invalid_tool_calls=[])
5b. Custom parser of your own¶
Any pydantic.BaseModel subclass works. Here's a tiny one for a binary yes/no with a confidence rating.
from pydantic import BaseModel, Field
from typing import Literal
class YesNoConfidence(BaseModel):
"""Answer yes/no and rate your confidence on a 1–5 scale."""
answer: Literal["yes", "no"] = Field(..., description="Your yes/no answer")
confidence: Literal[1, 2, 3, 4, 5] = Field(..., description="1=guess, 5=certain")
card_custom = ExpCardInit()
card_custom.proj_dir = PROJECT_DIR
card_custom.projectname = "mode3_custom_class"
card_custom.model = MODEL_NAME
card_custom.family = MODEL_FAMILY
card_custom.parameters = {"temperature": 0}
card_custom.task_file = task_file
card_custom.parser = YesNoConfidence # <-- your custom class
card_custom.cogtype = "no"
card_custom.nsim = 1
card_custom.tunnel_status = "0"
exp = ExpCard(card_custom)
scanner = ScannerModel(expcard=exp)
results_custom = scanner.run()
pprint(results_custom[0][0]["pred_resp"])
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/mode3_custom_class/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:39:10.973 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:10, 10.76s/it]
2it [00:21, 10.76s/it]
3it [00:31, 10.29s/it]
4it [00:49, 13.24s/it]
5it [01:02, 13.33s/it]
6it [01:12, 12.13s/it]
6it [01:12, 12.05s/it]
2026-05-07 11:40:23.415 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:40:23.435 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
AIMessage(content="{'answer': 'yes', 'confidence': 1}", additional_kwargs={}, response_metadata={}, id='55c67e05-bf49-410b-b4bc-371d1212ea35', tool_calls=[], invalid_tool_calls=[])
6. Mode 5 — callable dispatch (replaces parser="dynamic")¶
ps-parser change:
parser='dynamic'has been removed.
The hardcoded LangGraph routing it implied is gone. Use a callable instead — it is more flexible, explicit, and works with any task.
When parser is a callable (not a class), TaskRunner calls it once per trial
with the trcode string and uses the returned Pydantic class for that trial's
structured output. This gives arbitrary per-trial routing without any hardcoded logic.
Reality-monitoring example — encoding trials use Response_part_1_rm,
test trials use Response_part_2_rm:
from psychscanner.parsers import Response_part_1_rm, Response_part_2_rm
# Callable that maps trcode → parser class
def rm_dispatch(trcode: str):
if "test" in trcode:
return Response_part_2_rm # Judgment + Confidence
return Response_part_1_rm # Word_2 + Rating
# Preview routing before running
for tc in ["encode_1", "encode_2", "test_1", "test_2"]:
print(f" {tc:<12} → {rm_dispatch(tc).__name__}")
rm_task = {
"tasktype" : "survey",
"taskname" : "rm_callable_demo",
"instructions": {
"definition": [
"Encoding: report the second word and rate its relatedness 0-100.",
"Test: judge whether the word was internal/external and rate confidence 1-6."
]
},
"contexts" : ["encode", "test"],
"contexts_id" : ["encode", "test"],
"context_present": True,
"chain_type" : "item",
"parser" : "0", # card-level fallback; callable handles each trial
"items": {
"encode_1": [{"trcode": "encode_1", "stimulus": "Word 1: APPLE | Word 2: FRUIT"}],
"encode_2": [{"trcode": "encode_2", "stimulus": "Word 1: TABLE | Word 2: ____ (imagine)"}],
"test_1" : [{"trcode": "test_1", "stimulus": "APPLE — was the 2nd word internal or external?"}],
"test_2" : [{"trcode": "test_2", "stimulus": "TABLE — was the 2nd word internal or external?"}],
},
}
rm_task_file = PROJECT_DIR / "rm_callable_demo.json"
rm_task_file.write_text(json.dumps(rm_task, indent=2))
card_callable = ExpCardInit()
card_callable.proj_dir = PROJECT_DIR
card_callable.projectname = "mode5_callable"
card_callable.model = MODEL_NAME
card_callable.family = MODEL_FAMILY
card_callable.parameters = {"temperature": 0}
card_callable.task_file = rm_task_file
card_callable.parser = rm_dispatch # <-- callable dispatch
card_callable.cogtype = "no"
card_callable.nsim = 1
card_callable.tunnel_status = "0"
exp = ExpCard(card_callable)
results_callable = ScannerModel(expcard=exp).run()
for trial in results_callable[0]:
print(f"trcode={trial['trcode']!r:<14} parsed={trial['pred_resp']}")
encode_1 → Response_part_1_rm encode_2 → Response_part_1_rm test_1 → Response_part_2_rm test_2 → Response_part_2_rm ----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/mode5_callable/rm_callable_demo/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:40:23.830 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:11, 11.31s/it]
2it [00:25, 12.99s/it]
3it [00:32, 10.43s/it]
4it [00:40, 9.45s/it]
4it [00:40, 10.20s/it]
2026-05-07 11:41:04.703 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:41:04.748 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
trcode='encode_1' parsed=content="{'Word_2': 'FRUIT', 'Rating': 6.0}" additional_kwargs={} response_metadata={} id='f23bbce9-d501-4546-899e-f09dc73e3674' tool_calls=[] invalid_tool_calls=[]
trcode='encode_2' parsed=content="{'Word_2': 'TABLE', 'Rating': 0.5}" additional_kwargs={} response_metadata={} id='344b1f9c-9056-43b8-a38e-31d2dc0abeed' tool_calls=[] invalid_tool_calls=[]
trcode='test_1' parsed=content="{'Judgment': 'internal', 'Confidence': 6}" additional_kwargs={} response_metadata={} id='ca936845-d0a8-452e-a6a6-07a334863808' tool_calls=[] invalid_tool_calls=[]
trcode='test_2' parsed=content="{'Judgment': 'internal', 'Confidence': 6}" additional_kwargs={} response_metadata={} id='5a58c43f-d7ea-4ff5-b53c-30c8de3cd6fe' tool_calls=[] invalid_tool_calls=[]
Encoding trials return Word_2 + Rating (from Response_part_1_rm).
Test trials return Judgment + Confidence (from Response_part_2_rm).
The callable is called once per trial with its trcode; no LangGraph routing
nodes, no hardcoded strings.
Tip: You can also embed the parser name directly in each trial's JSON dict (Form A — see 08_ps_parser_guide.ipynb §6).
7. parser_raw=True — keep the raw AIMessage alongside parsing¶
Useful for debugging when you want to see what the model emitted before Pydantic coerced it.
card_raw = ExpCardInit()
card_raw.proj_dir = PROJECT_DIR
card_raw.projectname = "opt_parser_raw"
card_raw.model = MODEL_NAME
card_raw.family = MODEL_FAMILY
card_raw.parameters = {"temperature": 0}
card_raw.task_file = task_file
card_raw.parser = DefaultLiteralVivid15
card_raw.parser_raw = True # <-- keep raw alongside parsed
card_raw.cogtype = "no"
card_raw.nsim = 1
card_raw.tunnel_status = "0"
exp = ExpCard(card_raw)
scanner = ScannerModel(expcard=exp)
results_raw = scanner.run()
print("--- pred_resp with parser_raw=True ---")
pprint(results_raw[0][0]["pred_resp"])
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/opt_parser_raw/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:41:05.305 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:09, 9.15s/it]
2it [00:14, 6.65s/it]
3it [00:23, 7.77s/it]
4it [00:32, 8.25s/it]
5it [00:43, 9.33s/it]
6it [00:50, 8.75s/it]
6it [00:51, 8.50s/it]
2026-05-07 11:41:56.375 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:41:56.424 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
--- pred_resp with parser_raw=True ---
AIMessage(content='{\n "Vividness": 1\n}', additional_kwargs={}, response_metadata={'model': 'smollm2:360m-instruct-fp16', 'created_at': '2026-05-07T15:41:13.909807Z', 'done': True, 'done_reason': 'stop', 'total_duration': 7806047577, 'load_duration': 150795959, 'prompt_eval_count': 106, 'prompt_eval_duration': 1186519107, 'eval_count': 12, 'eval_duration': 1114978910, 'logprobs': None, 'model_name': 'smollm2:360m-instruct-fp16', 'model_provider': 'ollama'}, id='lc_run--019e0319-8aaa-7da2-a46b-3950c5d1cbee-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 106, 'output_tokens': 12, 'total_tokens': 118})
8. parser_config — controlling the structured-output method¶
parser_config is forwarded as **kwargs to model.with_structured_output(...). The default is {"method": "json_schema"}. Other LangChain-supported values include "function_calling" (tool-call based) and "json_mode" — availability depends on the provider.
from psychscanner.parsers import DefaultLiteralAgree
card_cfg = ExpCardInit()
card_cfg.proj_dir = PROJECT_DIR/"8"
card_cfg.projectname = "opt_parser_config"
card_cfg.model = MODEL_NAME
card_cfg.family = MODEL_FAMILY
card_cfg.parameters = {"temperature": 0}
card_cfg.task_file = task_file
card_cfg.parser = DefaultLiteralAgree
card_cfg.parser_config = {"method": "json_schema"} # try "function_calling" if your model supports it
card_cfg.cogtype = "no"
card_cfg.nsim = 1
card_cfg.tunnel_status = "0"
exp = ExpCard(card_cfg)
scanner = ScannerModel(expcard=exp)
results_cfg = scanner.run()
pprint(results_cfg[0][0]["pred_resp"])
----<PROJECT AND DATA ROOT DIRECTORY>----
Project root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/8
Simulation data root dir: /Users/saurabhext/Documents/PSYCHSCANNER/psychscanner/examples/_parser_tutorial_runs/8/opt_parser_config/example_survey/ollama_smollm2:360m-instruct-fp16_SingleTurn
----<>----
--<api key>-- warning: OLLAMA_API_KEY not set; proceeding without explicit api_key for family 'ollama'
--<chat model>-- model='smollm2:360m-instruct-fp16' temperature=0.0
2026-05-07 11:41:56.921 | CRITICAL | psychscanner.session_tunnel.session_tunnel:create_tunnel:152 - BEGIN
TOTAL RUNS: 1 RESUME IDX: None
----<>---- task running
0it [00:00, ?it/s]
1it [00:21, 21.17s/it]
2it [00:30, 13.93s/it]
3it [00:38, 11.44s/it]
4it [00:50, 11.60s/it]
5it [01:00, 11.09s/it]
6it [01:21, 14.54s/it]
6it [01:21, 13.63s/it]
2026-05-07 11:43:18.831 | INFO | psychscanner.session_tunnel.session_tunnel:scan_checkpoint:184 - scan-checkpoint
----<scanned runs>---- i = 0
2026-05-07 11:43:18.861 | CRITICAL | psychscanner.session_tunnel.session_tunnel:end_checkpoint:163 - END
AIMessage(content="{'rating': 5}", additional_kwargs={}, response_metadata={}, id='01f71ef6-b2ef-42fe-b89f-daad45548059', tool_calls=[], invalid_tool_calls=[])
9. Summary — quick reference¶
# Mode 1 — no parsing (works with mock-llm)
card.parser = "0" # or None
# Mode 2 — read parser name from task JSON 'parser' field (registry lookup)
card.parser = "1"
# Mode 3 — pass a registered name string directly
card.parser = "DefaultLiteralVivid15"
# Mode 4 — pass a Pydantic class directly (recommended)
card.parser = MyParserClass
# Mode 5 — callable dispatch; replaces removed parser='dynamic'
from psychscanner.parsers import Response_part_1_rm, Response_part_2_rm
card.parser = lambda trcode: Response_part_2_rm if "test" in trcode else Response_part_1_rm
# Cross-cutting flags
card.parser_raw = True # keep raw AIMessage alongside parsed output
card.parser_config = {"method": "json_schema"} # passed to with_structured_output()
Gotchas¶
mock-llmdoes not support structured output — only Mode 1 ("0") works with it.- Mode 2 (
"1") resolves viaget_parser()(registry lookup); noeval()required. parser='dynamic'has been removed in ps-parser. Use Mode 5 (callable) instead.- See 08_ps_parser_guide.ipynb for per-trial JSON routing (Form A).