mathpix 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 +7 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE +21 -0
- data/README.md +171 -0
- data/SECURITY.md +137 -0
- data/lib/mathpix/balanced_ternary.rb +86 -0
- data/lib/mathpix/batch.rb +155 -0
- data/lib/mathpix/capture_builder.rb +142 -0
- data/lib/mathpix/chemistry.rb +69 -0
- data/lib/mathpix/client.rb +439 -0
- data/lib/mathpix/configuration.rb +187 -0
- data/lib/mathpix/configuration.rb.backup +125 -0
- data/lib/mathpix/conversion.rb +257 -0
- data/lib/mathpix/document.rb +320 -0
- data/lib/mathpix/errors.rb +78 -0
- data/lib/mathpix/mcp/auth/oauth_provider.rb +346 -0
- data/lib/mathpix/mcp/auth/token_manager.rb +31 -0
- data/lib/mathpix/mcp/auth.rb +18 -0
- data/lib/mathpix/mcp/base_tool.rb +117 -0
- data/lib/mathpix/mcp/elicitations/ambiguity_elicitation.rb +162 -0
- data/lib/mathpix/mcp/elicitations/base_elicitation.rb +141 -0
- data/lib/mathpix/mcp/elicitations/confidence_elicitation.rb +162 -0
- data/lib/mathpix/mcp/elicitations.rb +78 -0
- data/lib/mathpix/mcp/middleware/cors_middleware.rb +94 -0
- data/lib/mathpix/mcp/middleware/oauth_middleware.rb +72 -0
- data/lib/mathpix/mcp/middleware/rate_limiting_middleware.rb +140 -0
- data/lib/mathpix/mcp/middleware.rb +13 -0
- data/lib/mathpix/mcp/resources/formats_list_resource.rb +113 -0
- data/lib/mathpix/mcp/resources/hierarchical_router.rb +237 -0
- data/lib/mathpix/mcp/resources/latest_snip_resource.rb +60 -0
- data/lib/mathpix/mcp/resources/recent_snips_resource.rb +75 -0
- data/lib/mathpix/mcp/resources/snip_stats_resource.rb +78 -0
- data/lib/mathpix/mcp/resources.rb +15 -0
- data/lib/mathpix/mcp/server.rb +174 -0
- data/lib/mathpix/mcp/tools/batch_convert_tool.rb +106 -0
- data/lib/mathpix/mcp/tools/check_document_status_tool.rb +66 -0
- data/lib/mathpix/mcp/tools/convert_document_tool.rb +90 -0
- data/lib/mathpix/mcp/tools/convert_image_tool.rb +91 -0
- data/lib/mathpix/mcp/tools/convert_strokes_tool.rb +82 -0
- data/lib/mathpix/mcp/tools/get_account_info_tool.rb +57 -0
- data/lib/mathpix/mcp/tools/get_usage_tool.rb +62 -0
- data/lib/mathpix/mcp/tools/list_formats_tool.rb +81 -0
- data/lib/mathpix/mcp/tools/search_results_tool.rb +111 -0
- data/lib/mathpix/mcp/transports/http_streaming_transport.rb +622 -0
- data/lib/mathpix/mcp/transports/sse_stream_handler.rb +236 -0
- data/lib/mathpix/mcp/transports.rb +12 -0
- data/lib/mathpix/mcp.rb +52 -0
- data/lib/mathpix/result.rb +364 -0
- data/lib/mathpix/version.rb +22 -0
- data/lib/mathpix.rb +229 -0
- metadata +283 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mathpix
|
4
|
+
module MCP
|
5
|
+
module Resources
|
6
|
+
# Formats List Resource Handler
|
7
|
+
#
|
8
|
+
# Returns available output formats for Mathpix OCR
|
9
|
+
# URI: mathpix://formats/list
|
10
|
+
#
|
11
|
+
# The geodesic path: resource handler + static data
|
12
|
+
module FormatsListResource
|
13
|
+
# Fetch formats list
|
14
|
+
#
|
15
|
+
# @param client [Mathpix::Client] optional client (not used, kept for consistency)
|
16
|
+
# @return [Hash] resource content
|
17
|
+
def self.fetch(client = nil)
|
18
|
+
formats = {
|
19
|
+
image_formats: [
|
20
|
+
{
|
21
|
+
name: "latex_styled",
|
22
|
+
description: "LaTeX with styling commands",
|
23
|
+
use_case: "Typesetting, display"
|
24
|
+
},
|
25
|
+
{
|
26
|
+
name: "text",
|
27
|
+
description: "Plain text representation",
|
28
|
+
use_case: "Simple text extraction"
|
29
|
+
},
|
30
|
+
{
|
31
|
+
name: "latex_list",
|
32
|
+
description: "Array of LaTeX expressions",
|
33
|
+
use_case: "Multiple equations"
|
34
|
+
},
|
35
|
+
{
|
36
|
+
name: "mathml",
|
37
|
+
description: "MathML markup language",
|
38
|
+
use_case: "Web display, accessibility"
|
39
|
+
},
|
40
|
+
{
|
41
|
+
name: "asciimath",
|
42
|
+
description: "AsciiMath notation",
|
43
|
+
use_case: "Simple math notation"
|
44
|
+
},
|
45
|
+
{
|
46
|
+
name: "text_display",
|
47
|
+
description: "Display-style text",
|
48
|
+
use_case: "Large equation display"
|
49
|
+
},
|
50
|
+
{
|
51
|
+
name: "latex_simplified",
|
52
|
+
description: "Simplified LaTeX",
|
53
|
+
use_case: "Basic LaTeX output"
|
54
|
+
},
|
55
|
+
{
|
56
|
+
name: "data",
|
57
|
+
description: "Full response with metadata",
|
58
|
+
use_case: "Complete API response"
|
59
|
+
},
|
60
|
+
{
|
61
|
+
name: "html",
|
62
|
+
description: "HTML markup",
|
63
|
+
use_case: "Web integration"
|
64
|
+
}
|
65
|
+
],
|
66
|
+
document_formats: [
|
67
|
+
{
|
68
|
+
name: "markdown",
|
69
|
+
description: "Markdown document format",
|
70
|
+
use_case: "Note-taking, documentation"
|
71
|
+
},
|
72
|
+
{
|
73
|
+
name: "latex",
|
74
|
+
description: "LaTeX document format",
|
75
|
+
use_case: "Academic papers, typesetting"
|
76
|
+
},
|
77
|
+
{
|
78
|
+
name: "html",
|
79
|
+
description: "HTML document format",
|
80
|
+
use_case: "Web publishing"
|
81
|
+
},
|
82
|
+
{
|
83
|
+
name: "docx",
|
84
|
+
description: "Microsoft Word document",
|
85
|
+
use_case: "Word processing"
|
86
|
+
},
|
87
|
+
{
|
88
|
+
name: "tex.zip",
|
89
|
+
description: "LaTeX with figures (zipped)",
|
90
|
+
use_case: "Complete LaTeX projects"
|
91
|
+
}
|
92
|
+
]
|
93
|
+
}
|
94
|
+
|
95
|
+
{
|
96
|
+
uri: "mathpix://formats/list",
|
97
|
+
mime_type: "application/json",
|
98
|
+
content: JSON.pretty_generate({
|
99
|
+
success: true,
|
100
|
+
image_formats_count: formats[:image_formats].length,
|
101
|
+
document_formats_count: formats[:document_formats].length,
|
102
|
+
formats: formats,
|
103
|
+
usage: {
|
104
|
+
image_capture: "Use with Mathpix.snap() or ConvertImageTool",
|
105
|
+
document_conversion: "Use with Mathpix.document() or ConvertDocumentTool"
|
106
|
+
}
|
107
|
+
})
|
108
|
+
}
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Mathpix
|
6
|
+
module MCP
|
7
|
+
module Resources
|
8
|
+
# Hierarchical URI router with parameter extraction
|
9
|
+
class HierarchicalRouter
|
10
|
+
attr_reader :client, :routes
|
11
|
+
|
12
|
+
MIN_LIMIT = 1
|
13
|
+
MAX_LIMIT = 100
|
14
|
+
|
15
|
+
def initialize(client)
|
16
|
+
@client = client
|
17
|
+
@routes = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def register_route(pattern:, handler:, methods:, description: nil, params: nil)
|
21
|
+
@routes[pattern] = {
|
22
|
+
handler: handler,
|
23
|
+
methods: Array(methods),
|
24
|
+
description: description,
|
25
|
+
params: params
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def match_route(path, method)
|
30
|
+
@routes.each do |pattern, route|
|
31
|
+
next unless route[:methods].include?(method)
|
32
|
+
|
33
|
+
params = extract_params(pattern, path)
|
34
|
+
next if params.nil?
|
35
|
+
|
36
|
+
return {
|
37
|
+
handler: route[:handler],
|
38
|
+
params: params,
|
39
|
+
route: route
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def route_request(path, method, query_params)
|
47
|
+
match = match_route(path, method)
|
48
|
+
raise RouteNotFoundError, "No route matches #{method.upcase} #{path}" if match.nil?
|
49
|
+
|
50
|
+
# Merge URI params with query params
|
51
|
+
all_params = match[:params].merge(query_params)
|
52
|
+
|
53
|
+
# Validate parameters
|
54
|
+
validate_params(all_params)
|
55
|
+
|
56
|
+
# Call handler method
|
57
|
+
send(match[:handler], all_params)
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_query_params(query_string)
|
61
|
+
return {} if query_string.nil? || query_string.empty?
|
62
|
+
|
63
|
+
params = {}
|
64
|
+
|
65
|
+
query_string.split('&').each do |param|
|
66
|
+
key, value = param.split('=', 2)
|
67
|
+
key = key.to_sym
|
68
|
+
value = CGI.unescape(value) if value
|
69
|
+
|
70
|
+
# Type coercion
|
71
|
+
params[key] = coerce_value(value)
|
72
|
+
end
|
73
|
+
|
74
|
+
params
|
75
|
+
end
|
76
|
+
|
77
|
+
def pagination_links(path:, limit:, offset:, total:, query_params:)
|
78
|
+
links = {}
|
79
|
+
|
80
|
+
# Self link
|
81
|
+
links[:self] = build_link(path, limit, offset, query_params)
|
82
|
+
|
83
|
+
# First link
|
84
|
+
links[:first] = build_link(path, limit, 0, query_params)
|
85
|
+
|
86
|
+
# Previous link (if not on first page)
|
87
|
+
if offset > 0
|
88
|
+
prev_offset = [offset - limit, 0].max
|
89
|
+
links[:prev] = build_link(path, limit, prev_offset, query_params)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Next link (if not on last page)
|
93
|
+
if offset + limit < total
|
94
|
+
next_offset = offset + limit
|
95
|
+
links[:next] = build_link(path, limit, next_offset, query_params)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Last link
|
99
|
+
last_offset = ((total - 1) / limit) * limit
|
100
|
+
links[:last] = build_link(path, limit, last_offset, query_params)
|
101
|
+
|
102
|
+
links
|
103
|
+
end
|
104
|
+
|
105
|
+
def select_fields(data, fields_param)
|
106
|
+
return data if fields_param.nil?
|
107
|
+
|
108
|
+
fields = fields_param.split(',').map(&:strip).map(&:to_sym)
|
109
|
+
data.select { |k, _| fields.include?(k) }
|
110
|
+
end
|
111
|
+
|
112
|
+
def relationship_links(resource)
|
113
|
+
links = {
|
114
|
+
self: "/snip/#{resource[:id]}"
|
115
|
+
}
|
116
|
+
|
117
|
+
links[:batch] = "/batch/#{resource[:batch_id]}" if resource[:batch_id]
|
118
|
+
|
119
|
+
links
|
120
|
+
end
|
121
|
+
|
122
|
+
def uri_templates
|
123
|
+
@routes.map do |pattern, route|
|
124
|
+
{
|
125
|
+
template: pattern.gsub(/:(\w+)/, '{\1}'),
|
126
|
+
description: route[:description],
|
127
|
+
methods: route[:methods],
|
128
|
+
params: route[:params]
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def extract_params(pattern, path)
|
136
|
+
pattern_parts = pattern.split('/')
|
137
|
+
path_parts = path.split('/')
|
138
|
+
|
139
|
+
return nil unless pattern_parts.length == path_parts.length
|
140
|
+
|
141
|
+
params = {}
|
142
|
+
|
143
|
+
pattern_parts.each_with_index do |part, index|
|
144
|
+
if part.start_with?(':')
|
145
|
+
param_name = part[1..].to_sym
|
146
|
+
param_value = path_parts[index]
|
147
|
+
|
148
|
+
# Validate ID format (alphanumeric, underscores, hyphens)
|
149
|
+
return nil unless param_value =~ /^[a-zA-Z0-9_-]+$/
|
150
|
+
|
151
|
+
params[param_name] = param_value
|
152
|
+
else
|
153
|
+
return nil unless part == path_parts[index]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
params
|
158
|
+
end
|
159
|
+
|
160
|
+
def coerce_value(value)
|
161
|
+
return nil if value.nil?
|
162
|
+
|
163
|
+
# Integer
|
164
|
+
return value.to_i if value =~ /^\d+$/
|
165
|
+
|
166
|
+
# Boolean
|
167
|
+
return true if value == 'true'
|
168
|
+
return false if value == 'false'
|
169
|
+
|
170
|
+
# Array (comma-separated)
|
171
|
+
if value.include?(',')
|
172
|
+
return value.split(',').map(&:strip)
|
173
|
+
end
|
174
|
+
|
175
|
+
# String
|
176
|
+
value
|
177
|
+
end
|
178
|
+
|
179
|
+
def validate_params(params)
|
180
|
+
# Validate limit
|
181
|
+
if params[:limit]
|
182
|
+
unless params[:limit].is_a?(Integer)
|
183
|
+
raise InvalidParameterError, 'limit must be a number'
|
184
|
+
end
|
185
|
+
|
186
|
+
unless params[:limit].between?(MIN_LIMIT, MAX_LIMIT)
|
187
|
+
raise InvalidParameterError, "limit must be between #{MIN_LIMIT} and #{MAX_LIMIT}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Validate offset
|
192
|
+
if params[:offset]
|
193
|
+
unless params[:offset].is_a?(Integer)
|
194
|
+
raise InvalidParameterError, 'offset must be a number'
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Validate ID format (if present)
|
199
|
+
if params[:id]
|
200
|
+
unless params[:id] =~ /^[a-zA-Z0-9_-]+$/
|
201
|
+
raise InvalidParameterError, 'id must be alphanumeric'
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def build_link(path, limit, offset, query_params)
|
207
|
+
params = query_params.dup
|
208
|
+
params[:limit] = limit
|
209
|
+
params[:offset] = offset
|
210
|
+
|
211
|
+
query_string = params.map { |k, v| "#{k}=#{v}" }.join('&')
|
212
|
+
"#{path}?#{query_string}"
|
213
|
+
end
|
214
|
+
|
215
|
+
# Placeholder handler methods
|
216
|
+
# These would be implemented based on actual resource handlers
|
217
|
+
|
218
|
+
def get_snip(params)
|
219
|
+
# Delegate to actual resource handler
|
220
|
+
{ id: params[:id], placeholder: true }
|
221
|
+
end
|
222
|
+
|
223
|
+
def get_snip_metadata(params)
|
224
|
+
{ id: params[:id], metadata: true }
|
225
|
+
end
|
226
|
+
|
227
|
+
def get_line(params)
|
228
|
+
{ id: params[:id], line_number: params[:line_number] }
|
229
|
+
end
|
230
|
+
|
231
|
+
def list_snips(params)
|
232
|
+
[]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mathpix
|
4
|
+
module MCP
|
5
|
+
module Resources
|
6
|
+
# Latest Snip Resource Handler
|
7
|
+
#
|
8
|
+
# Returns the most recent capture result
|
9
|
+
# URI: mathpix://snip/recent/latest
|
10
|
+
#
|
11
|
+
# The geodesic path: resource handler + client delegation
|
12
|
+
module LatestSnipResource
|
13
|
+
# Fetch latest snip data
|
14
|
+
#
|
15
|
+
# @param client [Mathpix::Client] Mathpix API client
|
16
|
+
# @return [Hash] resource content
|
17
|
+
def self.fetch(client)
|
18
|
+
recent = client.recent(limit: 1)
|
19
|
+
|
20
|
+
if recent.empty?
|
21
|
+
{
|
22
|
+
uri: "mathpix://snip/recent/latest",
|
23
|
+
mime_type: "application/json",
|
24
|
+
content: JSON.pretty_generate({
|
25
|
+
success: false,
|
26
|
+
message: "No recent captures found"
|
27
|
+
})
|
28
|
+
}
|
29
|
+
else
|
30
|
+
result = recent.first
|
31
|
+
{
|
32
|
+
uri: "mathpix://snip/recent/latest",
|
33
|
+
mime_type: "application/json",
|
34
|
+
content: JSON.pretty_generate({
|
35
|
+
success: true,
|
36
|
+
id: result.request_id,
|
37
|
+
created_at: result.timestamp,
|
38
|
+
latex: result.latex,
|
39
|
+
text: result.text,
|
40
|
+
confidence: result.confidence,
|
41
|
+
is_printed: result.printed?,
|
42
|
+
is_handwritten: result.handwritten?,
|
43
|
+
position: result.position
|
44
|
+
})
|
45
|
+
}
|
46
|
+
end
|
47
|
+
rescue Mathpix::Error => e
|
48
|
+
{
|
49
|
+
uri: "mathpix://snip/recent/latest",
|
50
|
+
mime_type: "application/json",
|
51
|
+
content: JSON.pretty_generate({
|
52
|
+
success: false,
|
53
|
+
error: e.message
|
54
|
+
})
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mathpix
|
4
|
+
module MCP
|
5
|
+
module Resources
|
6
|
+
# Recent Snips Resource Handler
|
7
|
+
#
|
8
|
+
# Returns recent capture results with pagination
|
9
|
+
# URI: mathpix://snip/recent?limit=N
|
10
|
+
#
|
11
|
+
# The geodesic path: resource handler + client delegation
|
12
|
+
module RecentSnipsResource
|
13
|
+
# Fetch recent snips data
|
14
|
+
#
|
15
|
+
# @param client [Mathpix::Client] Mathpix API client
|
16
|
+
# @param limit [Integer] number of results (default: 10, max: 100)
|
17
|
+
# @return [Hash] resource content
|
18
|
+
def self.fetch(client, limit: 10)
|
19
|
+
config = client.config
|
20
|
+
|
21
|
+
# Use configuration bounds instead of magic numbers
|
22
|
+
limit = config.sanitize_limit(limit)
|
23
|
+
|
24
|
+
# Log the request
|
25
|
+
config.log(:info, 'Fetching recent snips', { limit: limit })
|
26
|
+
|
27
|
+
recent = client.recent(limit: limit)
|
28
|
+
|
29
|
+
results = recent.map do |result|
|
30
|
+
{
|
31
|
+
id: result.request_id,
|
32
|
+
created_at: result.timestamp,
|
33
|
+
latex_preview: truncate(result.latex, 100),
|
34
|
+
text_preview: truncate(result.text, 100),
|
35
|
+
confidence: result.confidence,
|
36
|
+
is_printed: result.printed?,
|
37
|
+
is_handwritten: result.handwritten?
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
{
|
42
|
+
uri: "mathpix://snip/recent?limit=#{limit}",
|
43
|
+
mime_type: "application/json",
|
44
|
+
content: JSON.pretty_generate({
|
45
|
+
success: true,
|
46
|
+
limit: limit,
|
47
|
+
count: results.length,
|
48
|
+
results: results
|
49
|
+
})
|
50
|
+
}
|
51
|
+
rescue Mathpix::Error => e
|
52
|
+
{
|
53
|
+
uri: "mathpix://snip/recent?limit=#{limit}",
|
54
|
+
mime_type: "application/json",
|
55
|
+
content: JSON.pretty_generate({
|
56
|
+
success: false,
|
57
|
+
error: e.message
|
58
|
+
})
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Truncate text for preview
|
63
|
+
#
|
64
|
+
# @param text [String, nil] text to truncate
|
65
|
+
# @param max_length [Integer] maximum length
|
66
|
+
# @return [String, nil] truncated text
|
67
|
+
def self.truncate(text, max_length)
|
68
|
+
return nil unless text
|
69
|
+
return text if text.length <= max_length
|
70
|
+
"#{text[0...max_length]}..."
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mathpix
|
4
|
+
module MCP
|
5
|
+
module Resources
|
6
|
+
# Snip Stats Resource Handler
|
7
|
+
#
|
8
|
+
# Returns overall capture statistics
|
9
|
+
# URI: mathpix://snip/stats
|
10
|
+
#
|
11
|
+
# The geodesic path: resource handler + client delegation
|
12
|
+
module SnipStatsResource
|
13
|
+
# Fetch snip statistics
|
14
|
+
#
|
15
|
+
# @param client [Mathpix::Client] Mathpix API client
|
16
|
+
# @return [Hash] resource content
|
17
|
+
def self.fetch(client)
|
18
|
+
config = client.config
|
19
|
+
|
20
|
+
# Log the request
|
21
|
+
config.log(:info, 'Fetching snip statistics', { limit: config.max_limit })
|
22
|
+
|
23
|
+
# Get recent captures to compute stats
|
24
|
+
recent = client.recent(limit: config.max_limit)
|
25
|
+
|
26
|
+
# Compute statistics
|
27
|
+
stats = compute_stats(recent)
|
28
|
+
|
29
|
+
{
|
30
|
+
uri: "mathpix://snip/stats",
|
31
|
+
mime_type: "application/json",
|
32
|
+
content: JSON.pretty_generate({
|
33
|
+
success: true,
|
34
|
+
period: "last_100_captures",
|
35
|
+
stats: stats
|
36
|
+
})
|
37
|
+
}
|
38
|
+
rescue Mathpix::Error => e
|
39
|
+
{
|
40
|
+
uri: "mathpix://snip/stats",
|
41
|
+
mime_type: "application/json",
|
42
|
+
content: JSON.pretty_generate({
|
43
|
+
success: false,
|
44
|
+
error: e.message
|
45
|
+
})
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Compute statistics from results
|
50
|
+
#
|
51
|
+
# @param results [Array<Mathpix::Result>] recent results
|
52
|
+
# @return [Hash] statistics
|
53
|
+
def self.compute_stats(results)
|
54
|
+
return { total: 0 } if results.empty?
|
55
|
+
|
56
|
+
total = results.length
|
57
|
+
printed_count = results.count(&:printed?)
|
58
|
+
handwritten_count = results.count(&:handwritten?)
|
59
|
+
|
60
|
+
confidences = results.map(&:confidence).compact
|
61
|
+
avg_confidence = confidences.empty? ? 0.0 : (confidences.sum / confidences.length.to_f)
|
62
|
+
|
63
|
+
{
|
64
|
+
total: total,
|
65
|
+
printed: printed_count,
|
66
|
+
handwritten: handwritten_count,
|
67
|
+
average_confidence: avg_confidence.round(3),
|
68
|
+
confidence_distribution: {
|
69
|
+
high: confidences.count { |c| c >= Mathpix::Configuration::CONFIDENCE_HIGH },
|
70
|
+
medium: confidences.count { |c| c >= Mathpix::Configuration::CONFIDENCE_MEDIUM && c < Mathpix::Configuration::CONFIDENCE_HIGH },
|
71
|
+
low: confidences.count { |c| c < Mathpix::Configuration::CONFIDENCE_MEDIUM }
|
72
|
+
}
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mathpix
|
4
|
+
module MCP
|
5
|
+
module Resources
|
6
|
+
# Custom error classes
|
7
|
+
class ResourceError < StandardError; end
|
8
|
+
class RouteNotFoundError < ResourceError; end
|
9
|
+
class ResourceNotFoundError < ResourceError; end
|
10
|
+
class InvalidParameterError < ResourceError; end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require_relative 'resources/hierarchical_router'
|