Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion provider/anthropicprovider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,11 +555,25 @@ func citationAnnotations(citations []anthropic.TextCitationUnion) []message.Anno
var annotations []message.Annotation
for _, citation := range citations {
var regions message.AnnotatedRegions
if citation.Type == "char_location" {
// Anthropic reports the cited span differently per location type:
// character offsets, PDF page numbers, or content-block indices. Map
// each to a text-span region, matching the Python client.
switch citation.Type {
case "char_location":
startIndex, endIndex := int(citation.StartCharIndex), int(citation.EndCharIndex)
regions = message.AnnotatedRegions{
&message.TextSpanAnnotatedRegion{StartIndex: &startIndex, EndIndex: &endIndex},
}
case "page_location":
startIndex, endIndex := int(citation.StartPageNumber), int(citation.EndPageNumber)
regions = message.AnnotatedRegions{
&message.TextSpanAnnotatedRegion{StartIndex: &startIndex, EndIndex: &endIndex},
}
case "content_block_location":
startIndex, endIndex := int(citation.StartBlockIndex), int(citation.EndBlockIndex)
regions = message.AnnotatedRegions{
&message.TextSpanAnnotatedRegion{StartIndex: &startIndex, EndIndex: &endIndex},
}
}
annotations = append(annotations, &message.CitationAnnotation{
FileID: citation.FileID,
Expand Down
76 changes: 76 additions & 0 deletions provider/anthropicprovider/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,82 @@ func TestCharacterLocationCitationsBecomeAnnotatedRegions(t *testing.T) {
}
}

// citationRegionFromResponse runs the agent against a server returning a single
// text block whose citation is the given JSON object, then returns the decoded
// text-span region of that citation.
func citationRegionFromResponse(t *testing.T, citationJSON string) *message.TextSpanAnnotatedRegion {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{
"id":"msg_region_citation",
"type":"message",
"role":"assistant",
"model":"claude-3-5-sonnet-20241022",
"stop_reason":"end_turn",
"content":[{
"type":"text",
"text":"The answer cites a document.",
"citations":[`+citationJSON+`]
}],
"usage":{"input_tokens":10,"output_tokens":5}
}`)
}))
defer server.Close()

resp, err := newTestClient(t, server).RunText(t.Context(), "cite something").Collect()
if err != nil {
t.Fatal(err)
}

var citation *message.CitationAnnotation
for content := range resp.Contents() {
if text, ok := content.(*message.TextContent); ok && len(text.Annotations) > 0 {
citation, _ = text.Annotations[0].(*message.CitationAnnotation)
}
}
if citation == nil || len(citation.AnnotatedRegions) != 1 {
t.Fatalf("citation = %#v", citation)
}
span, ok := citation.AnnotatedRegions[0].(*message.TextSpanAnnotatedRegion)
if !ok {
t.Fatalf("annotated region = %#v, want a text span", citation.AnnotatedRegions[0])
}
return span
}

// Page-location citations carry PDF page numbers rather than character offsets;
// they must still surface as a text-span region, matching the Python client.
func TestPageLocationCitationsBecomeAnnotatedRegions(t *testing.T) {
span := citationRegionFromResponse(t, `{
"type":"page_location",
"cited_text":"page excerpt",
"document_index":0,
"document_title":"Document",
"start_page_number":3,
"end_page_number":5
}`)
if span.StartIndex == nil || *span.StartIndex != 3 || span.EndIndex == nil || *span.EndIndex != 5 {
t.Fatalf("annotated region = %#v, want [3, 5)", span)
}
}

// Content-block-location citations carry block indices; they must also surface
// as a text-span region.
func TestContentBlockLocationCitationsBecomeAnnotatedRegions(t *testing.T) {
span := citationRegionFromResponse(t, `{
"type":"content_block_location",
"cited_text":"block excerpt",
"document_index":0,
"document_title":"Document",
"start_block_index":1,
"end_block_index":4
}`)
if span.StartIndex == nil || *span.StartIndex != 1 || span.EndIndex == nil || *span.EndIndex != 4 {
t.Fatalf("annotated region = %#v, want [1, 4)", span)
}
}

func TestHostedServerToolContents(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
Expand Down
Loading