The module was declared as github.com/zzg/fathom, which is not where this repo lives and never has been. Nothing is published at that path, so `go get github.com/zzg/fathom` resolves to a 404 while the import paths in every file assert otherwise. git.z.gd is a real module path rather than a cosmetic correction: Gitea serves the go-import meta tag for it, <meta name="go-import" content="git.z.gd/zzg/fathom git https://git.z.gd/zzg/fathom.git"> and the repo answers unauthenticated over HTTPS, so `go get git.z.gd/zzg/fathom` resolves where the old path could not. Mechanical: the module line plus 46 import paths across 23 files. There is no go.sum to churn, since fathom has no third-party dependencies. go build, go vet, gofmt and the full test suite are clean under the new path, and the built binary still runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package ingest
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"git.z.gd/zzg/fathom/internal/model"
|
|
)
|
|
|
|
// classify maps a span onto one of Fathom's kinds.
|
|
//
|
|
// The kind drives colour and grouping in the UI, so a wrong answer is cosmetic
|
|
// rather than lossy — the original span name is always kept verbatim. That is
|
|
// why this is a heuristic and not a registry: the alternative is demanding that
|
|
// every emitter be taught to Fathom before its traces render usefully, which
|
|
// would defeat the point of speaking plain OTLP.
|
|
//
|
|
// Attributes are checked before names because they are the more deliberate
|
|
// signal. Claude Code sets span.type; OpenTelemetry's GenAI conventions set
|
|
// gen_ai.operation.name; OpenLLMetry sets llm.request.type.
|
|
func classify(name string, attrs model.Attrs) model.Kind {
|
|
if k, ok := classifyByAttrs(attrs); ok {
|
|
return k
|
|
}
|
|
return classifyByName(name)
|
|
}
|
|
|
|
func classifyByAttrs(attrs model.Attrs) (model.Kind, bool) {
|
|
if v, ok := attrs["span.type"]; ok {
|
|
switch v.Str {
|
|
case "llm_request":
|
|
return model.KindLLMCall, true
|
|
case "tool", "tool.execution", "tool.blocked_on_user":
|
|
return model.KindToolCall, true
|
|
case "interaction", "subagent", "compaction":
|
|
return model.KindAgentStep, true
|
|
case "mcp.rpc":
|
|
return model.KindMCP, true
|
|
case "hook":
|
|
return model.KindHook, true
|
|
}
|
|
}
|
|
if v, ok := attrs["gen_ai.operation.name"]; ok {
|
|
switch v.Str {
|
|
case "chat", "text_completion", "generate_content":
|
|
return model.KindLLMCall, true
|
|
case "execute_tool":
|
|
return model.KindToolCall, true
|
|
case "embeddings":
|
|
return model.KindRetrieval, true
|
|
}
|
|
}
|
|
// A span carrying a tool call id is a tool call, whatever it is named.
|
|
if _, ok := attrs["gen_ai.tool.call.id"]; ok {
|
|
return model.KindToolCall, true
|
|
}
|
|
if _, ok := attrs["tool_name"]; ok {
|
|
return model.KindToolCall, true
|
|
}
|
|
// Likewise, a span reporting a model and token counts is a model call.
|
|
if _, ok := attrs["gen_ai.request.model"]; ok {
|
|
return model.KindLLMCall, true
|
|
}
|
|
return model.KindOther, false
|
|
}
|
|
|
|
func classifyByName(name string) model.Kind {
|
|
n := strings.ToLower(name)
|
|
switch {
|
|
case strings.Contains(n, "mcp"):
|
|
return model.KindMCP
|
|
case strings.Contains(n, "hook"):
|
|
return model.KindHook
|
|
case strings.Contains(n, "tool"):
|
|
return model.KindToolCall
|
|
case strings.Contains(n, "llm"), strings.Contains(n, "chat"),
|
|
strings.Contains(n, "completion"), strings.Contains(n, "gen_ai"):
|
|
return model.KindLLMCall
|
|
case strings.Contains(n, "retriev"), strings.Contains(n, "embed"),
|
|
strings.Contains(n, "vector"), strings.Contains(n, "search"):
|
|
return model.KindRetrieval
|
|
case strings.Contains(n, "agent"), strings.Contains(n, "interaction"),
|
|
strings.Contains(n, "chain"), strings.Contains(n, "workflow"),
|
|
strings.Contains(n, "task"):
|
|
return model.KindAgentStep
|
|
}
|
|
return model.KindOther
|
|
}
|