# Threaded moderation

A reply often only makes sense next to the posts before it: "same to you" is harmless alone and an insult after one. Send who wrote the content and the earlier posts of its conversation with the upload, and CalmLens reads the content in that context. It still moderates only the content itself.

## Send the conversation with an upload

Add two optional fields to any upload, such as a comment checked with `client.text.classify()`:

| Field           | Description                                                                                      |
| --------------- | ------------------------------------------------------------------------------------------------ |
| `authorId`      | Your identifier for who wrote the content, such as a user ID. Optional.                          |
| `context.items` | Earlier posts in the conversation, **oldest first**. Each has `text` and an optional `authorId`. |

**JavaScript/TypeScript Server SDK**

```typescript
const reply = await client.text.classify({
  text: "Same to you",
  authorId: "user-2",
  context: {
    items: [
      { text: "Has anyone tried the new update?", authorId: "user-1" },
      { text: "You would say that", authorId: "user-3" },
    ],
  },
});
console.log(reply.classification.moderation.status, reply.context);
```

**python**

```python
reply = client.text.classify(
    text="Same to you",
    author_id="user-2",
    context={
        "items": [
            {"text": "Has anyone tried the new update?", "authorId": "user-1"},
            {"text": "You would say that", "authorId": "user-3"},
        ]
    },
)
print(reply["classification"]["moderation"]["status"], reply["context"])
```

**go**

```go
package main

import (
    "context"
    "fmt"
    "log"
    calmlens "github.com/CalmLens/sdks/go"
)

func example(client *calmlens.ServerClient) {
    reply, err := client.Text.Classify(context.Background(), calmlens.ClassifyTextOptions{
        Text:     "Same to you",
        AuthorID: calmlens.Ptr("user-2"),
        Context: &calmlens.ModerationContext{Items: []calmlens.ModerationContextItem{
            {Text: "Has anyone tried the new update?", AuthorID: calmlens.Ptr("user-1")},
            {Text: "You would say that", AuthorID: calmlens.Ptr("user-3")},
        }},
    })
    if err != nil { log.Fatal(err) }
    fmt.Println(reply.Classification.Moderation.Status, reply.Context)
}

```

**ruby**

```ruby
reply = client.text.classify(
  text: 'Same to you',
  author_id: 'user-2',
  context: {items: [
    {text: 'Has anyone tried the new update?', author_id: 'user-1'},
    {text: 'You would say that', author_id: 'user-3'}
  ]}
)
puts reply['classification']['moderation']['status'], reply['context']
```

**rust**

```rust
use calmlens::{models::*, ServerClient};

async fn example(client: &ServerClient) -> Result<(), Box<dyn std::error::Error>> {
    let reply = client.text().classify(ClassifyText {
        text: "Same to you".into(),
        author_id: Some("user-2".into()),
        context: Some(ModerationContext::new([
            ModerationContextItem::new("Has anyone tried the new update?").author_id("user-1"),
            ModerationContextItem::new("You would say that").author_id("user-3"),
        ])),
        ..Default::default()
    }).await?;
    println!("{:?} {:?}", reply.classification.moderation.status, reply.context);
    Ok(())
}
```

**java**

```java
import com.calmlens.CalmLensServerClient;
import com.calmlens.Models.*;
import java.util.List;

public class Example {
  public static void example(CalmLensServerClient client) {
    Asset reply = client.text.classify(new ClassifyText()
        .text("Same to you")
        .authorId("user-2")
        .context(new ModerationContext(List.of(
            new ModerationContextItem("Has anyone tried the new update?", "user-1"),
            new ModerationContextItem("You would say that", "user-3")))));
    System.out.println(reply.classification().moderation().status() + " " + reply.context());
  }
}
```

**curl**

```bash
curl -X POST "https://api.calmlens.com/v1/projects/$CALMLENS_PROJECT_ID/assets?queue=false" \
  -H "X-Api-Key: $CALMLENS_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "name": "reply",
  "content": { "plainText": "Same to you", "type": "text/plain" },
  "authorId": "user-2",
  "context": {
    "items": [
      { "text": "Has anyone tried the new update?", "authorId": "user-1" },
      { "text": "You would say that", "authorId": "user-3" }
    ]
  }
}'
```

Author IDs only tell CalmLens which posts share a speaker, so it can see whether the content replies to, quotes or addresses someone else. They are compared with each other and never interpreted, so any stable opaque value works. Don't send names or descriptions of the people in the thread.

The fields work for every type of upload: text, images, documents, websites, audio and video. A meme posted as a reply is read against the posts before it in the same way as text. Images and documents a website or document contains are read with the same conversation as the upload they came from.

## What the conversation can and cannot do

CalmLens uses the earlier posts to read the content:

- who it addresses, and what words such as "you", "they" or "this" refer to;
- whether it quotes, reports or counters something said earlier, or is sarcastic;
- whether it continues or endorses an earlier post.

The earlier posts are never moderated themselves, and nothing in them is flagged on the content's behalf. A harmful earlier post doesn't make a harmless reply harmful, and context never creates a category the content doesn't contain. Instructions written in earlier posts are ignored.

A missing conversation is never treated as evidence that content is quoting, reporting or countering something, so leaving it out never makes content more likely to be allowed. Content sent without a conversation is classified on its own.

## Limits

| Limit                                   | Value                                          |
| --------------------------------------- | ---------------------------------------------- |
| Earlier posts per request               | Up to 20                                       |
| Characters in one earlier post          | Up to 2,000                                    |
| Characters of earlier posts per request | Up to 10,000                                   |
| `authorId` length                       | Up to 256 characters                           |
| Earlier posts CalmLens uses             | The most recent 5 that fit in 2,000 characters |

Characters are counted as Unicode code points, so an emoji counts once. CalmLens walks back from the most recent post and stops at the fifth post, or at the first older post that would take the total over 2,000 characters. The most recent post is always used. A request over the limits is refused with a `validation/` error naming the field.

The SDKs send only the posts CalmLens uses. A raw file upload carries the conversation in its query string as JSON (`context={"items":[...]}`), alongside the other asset fields. [Upload presets](/docs/guides/upload-presets) don't accept a conversation from the browser, since the browser could supply a misleading one: classify replies from your server, or upload the file through a preset and send the conversation with a server-side upload.

## Results, storage and billing

The asset records `authorId` and the `context` CalmLens used, so you can see what a decision was based on, and [retries](/docs/guides/asset-lifecycle) and [detailed reports](/docs/guides/classification-results) read the same conversation. The earlier posts are other people's text, so they're kept only as long as the content: when the content is removed, after processing unless `processingOptions.keepContentAfterProcessing` is on, by a purge or on request, `context` becomes `null`. `authorId` stays with the asset, like `externalId`.

Earlier posts are not billed. Text moderation is billed for the content's own tokens, as on the [pricing page](/docs/pricing), whatever conversation you send.

An identical upload with an identical conversation reuses the earlier result. The same content with a different conversation is classified again.
