|
| 1 | +# How to Build and Run Communication Protocol Queries |
| 2 | + |
| 3 | +This guide explains the end-to-end workflow for turning a reporting idea into a runnable query on the Learning Observer communication protocol. Follow the steps in order—both humans and language models can use this as a checklist when creating or automating queries. |
| 4 | + |
| 5 | +## 1. Frame the Data Task |
| 6 | + |
| 7 | +1. Write a one-sentence description of the insight or dataset you need (e.g., *“Return the latest writing sample for each student in a course”*). |
| 8 | +2. Identify the reducers, helper functions, or key-value store documents that expose the data. Concept docs provide summaries of the executor lifecycle, node types, and utilities for transforming reducer outputs. 【F:docs/concepts/communication_protocol.md†L1-L100】 |
| 9 | +3. Check whether an existing helper (e.g., `generate_base_dag_for_student_reducer`) already provides most of the DAG structure. Reusing helpers keeps queries consistent and concise. 【F:docs/concepts/communication_protocol.md†L62-L87】 |
| 10 | + |
| 11 | +## 2. Confirm Your Goal and Required Data |
| 12 | + |
| 13 | +1. Identify the data source(s): |
| 14 | + |
| 15 | + * **Reducers** - Aggregated documents stored in the key-value store. |
| 16 | + * **Helper functions** - Python callables published with `publish_function`. |
| 17 | + * **Roster/metadata** - Collections that need to be joined with reducer data. |
| 18 | +2. Decide which fields must appear in the output. Note whether you need the entire document or only specific fields. |
| 19 | +3. List all runtime values (course ID, time range, student list, etc.). These become `parameter` nodes later. |
| 20 | + |
| 21 | +Document these choices (in comments or metadata) so you can refer to them in later steps. |
| 22 | + |
| 23 | +## 3. Declare Parameters and Defaults |
| 24 | + |
| 25 | +Each runtime input must be expressed as a `parameter` node. Parameters can be required or optional and may include default values: |
| 26 | + |
| 27 | +```python |
| 28 | +course_id = query.parameter("course_id", required=True) |
| 29 | +student_id = query.parameter("student_id", required=False, default=None) |
| 30 | +``` |
| 31 | + |
| 32 | +For each parameter, document: |
| 33 | + |
| 34 | +* **Name** - Identifier passed to the DAG node. |
| 35 | +* **Type** - String, UUID, ISO date, etc. |
| 36 | +* **Required** - Boolean flag. |
| 37 | +* **Default** - Optional fallback value. |
| 38 | + |
| 39 | +> Tip: Emit parameter declarations first so later steps can reuse variables like `course_id["variable"]` consistently. |
| 40 | +
|
| 41 | +For fixed values (e.g., reducer names or field lists), define constants once near where they are used. |
| 42 | + |
| 43 | +## 4. Plan the Data Flow (DAG Skeleton) |
| 44 | + |
| 45 | +Translate the goal into a linear sequence of operations. A typical reducer query involves: |
| 46 | + |
| 47 | +1. Fetching roster metadata or other context. |
| 48 | +2. Producing keys for each entity (`keys` nodes). |
| 49 | +3. Retrieving reducer documents with `select`. |
| 50 | +4. Joining reducer outputs with metadata. |
| 51 | +5. (Optional) Post-processing with `map` or `call`. |
| 52 | + |
| 53 | +Example outline: |
| 54 | + |
| 55 | +``` |
| 56 | +roster = call("get_course_roster", course_id) |
| 57 | +reducer_keys = keys(reducer_name, roster.students) |
| 58 | +reducer_docs = select(reducer_keys, fields=[...]) |
| 59 | +enriched = join(reducer_docs, roster, left_on="student.id", right_on="id") |
| 60 | +export enriched |
| 61 | +``` |
| 62 | + |
| 63 | +Verify that every step depends only on earlier outputs, and adjust until the flow is acyclic. |
| 64 | + |
| 65 | +## 5. Construct Nodes with Query Helpers |
| 66 | + |
| 67 | +Use `query.py` helpers to implement the skeleton: |
| 68 | + |
| 69 | +```python |
| 70 | +from learning_observer.communication_protocol import query |
| 71 | + |
| 72 | +roster = query.call("get_course_roster", args={"course_id": course_id}) |
| 73 | +reducer_keys = query.keys( |
| 74 | + reducer="reading_fluency", |
| 75 | + entities=query.variable(roster, "students"), |
| 76 | +) |
| 77 | +reducer_docs = query.select( |
| 78 | + keys=reducer_keys, |
| 79 | + fields=query.SelectFields.SUMMARY, |
| 80 | +) |
| 81 | +enriched = query.join( |
| 82 | + left=reducer_docs, |
| 83 | + right=query.variable(roster), |
| 84 | + left_on="student_id", |
| 85 | + right_on="id", |
| 86 | +) |
| 87 | +``` |
| 88 | + |
| 89 | +Guidelines: |
| 90 | + |
| 91 | +* Use `query.variable(node, path=None)` for downstream access to prior outputs. |
| 92 | +* Encapsulate repeated or complex logic in functions for reuse and testing. |
| 93 | +* Use explicit names and keyword arguments—avoid positional arguments for clarity. |
| 94 | + |
| 95 | +## 6. Define Exports and Integrations |
| 96 | + |
| 97 | +Choose which nodes should be externally accessible: |
| 98 | + |
| 99 | +```python |
| 100 | +exports = { |
| 101 | + "reading_fluency": query.export("reading_fluency", enriched) |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +If integrating with the async helper layer, pass `exports` to `learning_observer.communication_protocol.integration.bind_exports`. |
| 106 | + |
| 107 | +Document required parameters and defaults with the export definitions. |
| 108 | + |
| 109 | +## 7. Flatten, Validate, and Serialise |
| 110 | + |
| 111 | +1. Convert the nested DAG into executor-ready form: |
| 112 | + |
| 113 | + ```python |
| 114 | + from learning_observer.communication_protocol import util |
| 115 | + dag = util.flatten(exports) |
| 116 | + ``` |
| 117 | + |
| 118 | +2. Confirm all node IDs are unique and reference earlier nodes. Inspect the flattened DAG if generated automatically. |
| 119 | + |
| 120 | +3. Serialise to JSON (e.g., `json.dumps(dag)`) when sending over the wire. |
| 121 | + |
| 122 | +4. Add automated tests—at minimum a smoke test against a fixture store. |
| 123 | + |
| 124 | +## 8. Expose the DAG to Clients |
| 125 | + |
| 126 | +To make the DAG discoverable over the websocket interface: |
| 127 | + |
| 128 | +* Define `EXECUTION_DAG` in the module file and register it with the loader. |
| 129 | +* On server start, the DAG will be advertised under the module’s namespace. |
| 130 | + |
| 131 | +Production deployments should prefer predefined DAGs for security. Open-query mode is optional and must be explicitly enabled. |
| 132 | + |
| 133 | +## 9. Execute the Query |
| 134 | + |
| 135 | +Submit the flattened DAG to the communication protocol endpoint with runtime parameters: |
| 136 | + |
| 137 | +```json |
| 138 | +{ |
| 139 | + "parameters": { |
| 140 | + "course_id": "course-123", |
| 141 | + "start_date": "2023-09-01" |
| 142 | + }, |
| 143 | + "exports": ["reading_fluency"], |
| 144 | + "dag": { ... flattened nodes ... } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +On success, the response includes export payloads keyed by export name. Inspect `DAGExecutionException` for error details. |
| 149 | + |
| 150 | +When using integration bindings, call the generated async function with the same parameters. |
| 151 | + |
| 152 | +## 10. Construct Websocket Requests |
| 153 | + |
| 154 | +Clients interact with `/wsapi/communication_protocol` via JSON messages. Each message contains: |
| 155 | + |
| 156 | +* `execution_dag` - Name of a predefined DAG or a full DAG object. |
| 157 | +* `target_exports` - List of exports to run. |
| 158 | +* `kwargs` - Runtime parameters. |
| 159 | + |
| 160 | +Example: |
| 161 | + |
| 162 | +```json |
| 163 | +{ |
| 164 | + "docs_request": { |
| 165 | + "execution_dag": "writing_observer", |
| 166 | + "target_exports": ["docs_with_roster"], |
| 167 | + "kwargs": { "course_id": "COURSE-123" } |
| 168 | + } |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +The server streams back updates in messages shaped like: |
| 173 | + |
| 174 | +```json |
| 175 | +[ |
| 176 | + { |
| 177 | + "op": "update", |
| 178 | + "path": "students.student-1", |
| 179 | + "value": { "text": "...", "provenance": { ... } } |
| 180 | + } |
| 181 | +] |
| 182 | +``` |
| 183 | + |
| 184 | +If `rerun_dag_delay` is set, the server automatically re-executes the DAG and pushes updates. |
| 185 | + |
| 186 | +## 11. Iterate and Maintain |
| 187 | + |
| 188 | +* Profile slow queries; large joins may need new helpers or precomputed reducers. |
| 189 | +* Keep DAGs version-controlled. Update dependent queries when reducers or helpers change. |
| 190 | +* Review security before exposing exports to untrusted clients. |
| 191 | + |
| 192 | +## 12. Test End-to-End |
| 193 | + |
| 194 | +* **Unit-test** reducers and helpers independently. |
| 195 | +* **Reference** `learning_observer/learning_observer/communication_protocol/test_cases.py` for DAG tests. |
| 196 | +* **Exercise websocket flows** manually or with automated integration tests. |
| 197 | + |
| 198 | +## 13. Document Parameters and Outputs |
| 199 | + |
| 200 | +Update module documentation with: |
| 201 | + |
| 202 | +* Export descriptions, parameter types, and return structures. |
| 203 | +* Sample request payloads. |
| 204 | +* Notes on authentication or runtime context. |
| 205 | + |
| 206 | +Good documentation ensures developers and tooling can invoke queries reliably. |
| 207 | + |
| 208 | +### Summary |
| 209 | + |
| 210 | +Following this workflow ensures queries are consistent, testable, and safe to expose across dashboards, notebooks, and automation tools. |
0 commit comments