terret-morph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6dc795139de48f9e15f00647df387d36f94aba7592661b3a070209ac6a9847e2
4
+ data.tar.gz: dfa94e68912825fb939d5843dce02fe305ec04aac71afc24a5df869c74bdafb3
5
+ SHA512:
6
+ metadata.gz: 1bd98fc5d45df8d52985553ad15b62c0ae01879184755f5f0af6553f160b93f640c6fc2ab4fb2a43d3b73de1f0fcac36f5150e96bdfff6349ff81de1c8f32433
7
+ data.tar.gz: d93c9e77f8a0bf16e511c7679875d3d939c6657c83bfe7840df30d7734ea00d2fc20fd56354ef9818f5eb4f88204a1ca51bf293ea4797e6da9733871ad2ea3de
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Terret
8
+ module Morph
9
+ # ctx[:summarizer] backed by Morph's Compact API (POST /v1/compact):
10
+ # extractive line-level compression, surviving lines byte-identical. Wire
11
+ # shape mirrors the deployed agora integration (Morph::CompactClient).
12
+ # Every failure declines to nil with a warn — compaction is an
13
+ # optimization, and the Compactor skips the boundary when declined. All
14
+ # knobs are read per call, so reconfigure is live by construction.
15
+ class Summarizer < Hames::Service
16
+ service_key :summarizer
17
+ # transport: is an injectable seam (tests pass a callable), not YAML config.
18
+ config_schema compression_ratio: { type: Numeric, default: 0.4,
19
+ doc: "target fraction of the original token count" },
20
+ api_key: { type: String,
21
+ doc: "Morph key; falls back to ENV MORPH_API_KEY when unset" },
22
+ api_base: { type: String, default: "https://api.morphllm.com/v1",
23
+ doc: "Morph Compact API base URL" },
24
+ timeout: { type: Numeric, default: 30.0,
25
+ doc: "seconds a compaction request may run" }
26
+
27
+ DEFAULT_BASE = "https://api.morphllm.com/v1"
28
+ DEFAULT_TIMEOUT = 30.0
29
+ DEFAULT_RATIO = 0.4
30
+
31
+ def start(_ctx); end
32
+
33
+ def reconfigure(_config); end # knobs are read per call
34
+
35
+ def summarize(history)
36
+ key = api_key
37
+ return decline("MORPH_API_KEY not configured") if key.nil? || key.empty?
38
+
39
+ body = JSON.generate({ input: render(history),
40
+ compression_ratio: config[:compression_ratio] || DEFAULT_RATIO,
41
+ preserve_recent: 0 })
42
+ status, response = transport.call("#{api_base}/compact",
43
+ { "Authorization" => "Bearer #{key}",
44
+ "Content-Type" => "application/json" },
45
+ body)
46
+ return decline("HTTP #{status}") unless (200..299).cover?(status)
47
+
48
+ parsed = JSON.parse(response.to_s)
49
+ return decline("unexpected response shape: #{parsed.class}") unless parsed.is_a?(Hash)
50
+
51
+ output = parsed["output"]
52
+ return decline("non-string output: #{output.class}") unless output.is_a?(String)
53
+
54
+ output.empty? ? decline("empty output") : output
55
+ rescue JSON::ParserError => e
56
+ decline("invalid JSON: #{e.message}")
57
+ rescue StandardError => e
58
+ decline("#{e.class}: #{e.message}")
59
+ end
60
+
61
+ private
62
+
63
+ def api_key = config[:api_key] || ENV["MORPH_API_KEY"]
64
+ def api_base = config[:api_base] || ENV["MORPH_API_BASE"] || DEFAULT_BASE
65
+
66
+ # timeout=0 must not mean "no timeout" (the agora/Faraday lesson):
67
+ # floor anything non-positive back to the default.
68
+ def timeout
69
+ configured = (config[:timeout] || ENV["MORPH_COMPACT_TIMEOUT"]).to_f
70
+ configured.positive? ? configured : DEFAULT_TIMEOUT
71
+ end
72
+
73
+ # The transcript Morph compresses: role-tagged lines, one per message
74
+ # part so line-level compression can keep or drop each on its merit.
75
+ # Extractive compression keeps surviving lines byte-identical, so the
76
+ # compacted history the model sees is a strict subset of what it already
77
+ # saw. Tool calls and results carry their own line rather than rendering
78
+ # blank: a session's deploy ids, arguments, and errors live there, and a
79
+ # summary that dropped them would be worse than no summary at all.
80
+ def render(history)
81
+ history.flat_map { |m| m.parts.map { |part| "#{m.role}: #{line_for(part)}" } }.join("\n")
82
+ end
83
+
84
+ def line_for(part)
85
+ case part
86
+ when LLM::Text then part.text
87
+ when LLM::ToolCall then "[tool_call #{part.name} #{JSON.generate(part.args)}]"
88
+ when LLM::ToolResult then "[tool_result #{part.error || part.content}]"
89
+ else part.to_s
90
+ end
91
+ end
92
+
93
+ def transport
94
+ config[:transport] || method(:http_post)
95
+ end
96
+
97
+ def http_post(url, headers, body)
98
+ uri = URI(url)
99
+ http = Net::HTTP.new(uri.host, uri.port)
100
+ http.use_ssl = uri.scheme == "https"
101
+ http.open_timeout = timeout
102
+ http.read_timeout = timeout
103
+ http.write_timeout = timeout
104
+ response = http.post(uri.request_uri, body, headers)
105
+ [response.code.to_i, response.body]
106
+ end
107
+
108
+ def decline(message)
109
+ warn "terret-morph: compact declined: #{message}"
110
+ nil
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "terret"
5
+ rescue LoadError
6
+ require_relative "../../../terret-core/lib/terret" # monorepo path source
7
+ end
8
+
9
+ require_relative "morph/summarizer"
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: terret-morph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Obie Fernandez
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: terret-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ description: 'ctx[:summarizer] backed by Morph''s Compact API: extractive line-level
27
+ compression (surviving lines byte-identical), wire mirrored from the deployed agora
28
+ integration. Zero runtime dependencies beyond stdlib net/http.'
29
+ email:
30
+ - obiefernandez@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - lib/terret/morph.rb
36
+ - lib/terret/morph/summarizer.rb
37
+ homepage: https://terret.org
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://terret.org
42
+ source_code_uri: https://github.com/terret-org/terret
43
+ bug_tracker_uri: https://github.com/terret-org/terret/issues
44
+ rubygems_mfa_required: 'true'
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '4.0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubygems_version: 4.0.16
60
+ specification_version: 4
61
+ summary: Morph-backed context compaction for Terret
62
+ test_files: []