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
9 changes: 6 additions & 3 deletions tool/lib/src/commands/generate_skill_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;

import '../models/skill_params.dart';
import '../services/gemini_service.dart';
import '../services/resource_fetcher_service.dart';
import 'base_skill_command.dart';

/// Command to generate skills from a configuration file.
Expand Down Expand Up @@ -46,10 +47,12 @@ class GenerateSkillCommand extends BaseSkillCommand {
}

try {
final combinedMarkdown = await fetchAndConvertContent(
final fetcher = ResourceFetcherService(
httpClient: httpClient,
logger: logger,
);
final combinedMarkdown = await fetcher.fetchAndConvertContent(
skill.resources,
httpClient,
logger,
configDir: configDir,
);

Expand Down
9 changes: 6 additions & 3 deletions tool/lib/src/commands/validate_skill_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:path/path.dart' as p;

import '../models/skill_params.dart';
import '../services/gemini_service.dart';
import '../services/resource_fetcher_service.dart';
import 'base_skill_command.dart';

/// Command to validate skills by re-generating and comparing with existing skills.
Expand Down Expand Up @@ -52,10 +53,12 @@ class ValidateSkillCommand extends BaseSkillCommand {

try {
// Re-generate markdown content
final markdown = await fetchAndConvertContent(
final fetcher = ResourceFetcherService(
httpClient: httpClient,
logger: logger,
);
final markdown = await fetcher.fetchAndConvertContent(
skill.resources,
httpClient,
logger,
configDir: configDir,
);

Expand Down
118 changes: 9 additions & 109 deletions tool/lib/src/services/gemini_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dar
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:retry/retry.dart';
import 'package:yaml_writer/yaml_writer.dart';

import 'markdown_converter.dart';
import 'prompts.dart';
import 'skill_instructions.dart';

/// Service for interacting with the Gemini API to generate and validate skills.
Expand Down Expand Up @@ -71,7 +70,7 @@ class GeminiService {
}) async {
final service = GenerativeService(client: _client);
final lastModified = io.HttpDate.format(DateTime.now());
final prompt = _createSkillPrompt(rawMarkdown, instructions);
final prompt = Prompts.createSkillPrompt(rawMarkdown, instructions);

final request = _createRequest(
prompt,
Expand Down Expand Up @@ -128,32 +127,13 @@ class GeminiService {
int thinkingBudget = defaultThinkingBudget,
}) async {
final service = GenerativeService(client: _client);
final validationPrompt =
'''
Validate the following skill document against the provided source material and verify if it is valid.
Focus on:
1. Accuracy: Does the skill capture the technical details correctly based on the Source Material?
2. Structure: Is the skill well-structured according to skill best practices?
3. Completeness: Is any critical information missing in the skill that is present in the Source Material?

Context:
- The skill was originally generated on: $generationDate
- The current evaluation is using model: $modelName
- The instructions used to generate the skill were:
$instructions

Source Material:
$markdown

Current Skill Content:
"$currentSkillContent"
---

Grade the current output based on the instructions and the comparison to current website content and instructions today.
Establish a conclusion on whether the new skill is valid or not.
Reasons for a good or bad quality grade should be provided including concepts such as missing content, different model used, more than a few months old, etc.
On the very last line, output "Grade: [0-100]" representing overall quality of the skill compared to the assumed value if it were generated again today.
''';
final validationPrompt = Prompts.validateExistingSkillContentPrompt(
markdown,
instructions,
generationDate,
modelName,
currentSkillContent,
);

final request = _createRequest(
validationPrompt,
Expand Down Expand Up @@ -223,24 +203,6 @@ On the very last line, output "Grade: [0-100]" representing overall quality of t
return '${cleaned.trim()}\n';
}

String _createSkillPrompt(String markdown, String? instructions) {
return '''
Rewrite the following technical documentation into a high-quality "SKILL.md" file.

DO NOT include any YAML frontmatter. Start immediately with the markdown content (e.g. headers).

**Guidelines:**
1. **Ignore Noise**: Exclude navigation bars, footers, "Edit this page" links, and other non-technical content.
2. **Decision Trees**: If the content describes a process with multiple choices or steps, YOU MUST create a "Decision Logic" or "Flowchart" section to guide the agent.
3. **Clarity**: Use clear headings, bullet points, and code blocks.
4. **Format**: Do NOT wrap the entire output in a markdown code block (like ```markdown ... ```). Return raw markdown text.
${instructions != null && instructions.isNotEmpty ? '5. **Special Instructions**: $instructions' : ''}

Raw Content:
$markdown
''';
}

GenerateContentRequest _createRequest(
String prompt, {
String? systemInstruction,
Expand Down Expand Up @@ -282,65 +244,3 @@ class _ApiKeyClient extends http.BaseClient {
return _inner.send(request);
}
}

/// Fetches and converts content from a list of resources.
///
/// Throws an [Exception] if fetching any resource fails. This strict behavior
/// prevents wasting Gemini tokens on generating low-quality skills when
/// source material is missing.
Future<String> fetchAndConvertContent(
List<String> resources,
http.Client httpClient,
Logger logger, {
io.Directory? configDir,
}) async {
final converter = MarkdownConverter();
final sb = StringBuffer();
for (final resource in resources) {
logger.info(' Fetching $resource...');

if (resource.startsWith('http://')) {
throw Exception(
'Insecure HTTP URL found: $resource. '
'Only HTTPS URLs or relative file paths are allowed.',
);
}

if (resource.startsWith('https://')) {
final response = await httpClient.get(Uri.parse(resource));
if (response.statusCode == 200) {
sb
..writeln('--- Raw content from $resource ---')
..writeln(converter.convert(response.body));
} else {
throw Exception(
'Failed to fetch $resource: HTTP ${response.statusCode}. '
'Failing fast to save Gemini tokens.',
);
}
} else {
if (configDir == null) {
throw Exception(
'Relative resource "$resource" found, but no configuration '
'directory was provided to resolve it.',
);
}
final file = io.File(p.join(configDir.path, resource));
if (!file.existsSync()) {
throw Exception('Local resource file not found: ${file.path}');
}

final String content;
try {
content = file.readAsStringSync();
} on io.FileSystemException {
throw Exception('Local resource file is not readable: ${file.path}');
}

sb
..writeln('--- Raw content from $resource ---')
..writeln(content);
}
}
return sb.toString();
}
62 changes: 62 additions & 0 deletions tool/lib/src/services/prompts.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: avoid_classes_with_only_static_members

/// Manages prompts used by the GeminiService.
class Prompts {
/// Creates the prompt for generating a new skill.
static String createSkillPrompt(String markdown, String? instructions) {
return '''
Rewrite the following technical documentation into a high-quality "SKILL.md" file.

DO NOT include any YAML frontmatter. Start immediately with the markdown content (e.g. headers).

**Guidelines:**
1. **Ignore Noise**: Exclude navigation bars, footers, "Edit this page" links, and other non-technical content.
2. **Decision Trees**: If the content describes a process with multiple choices or steps, YOU MUST create a "Decision Logic" or "Flowchart" section to guide the agent.
3. **Clarity**: Use clear headings, bullet points, and code blocks.
4. **Format**: Do NOT wrap the entire output in a markdown code block (like ```markdown ... ```). Return raw markdown text.
${instructions != null && instructions.isNotEmpty ? '5. **Special Instructions**: $instructions' : ''}

Raw Content:
$markdown
''';
}

/// Creates the prompt for validating an existing skill.
static String validateExistingSkillContentPrompt(
String markdown,
String instructions,
String generationDate,
String modelName,
String currentSkillContent,
) {
return '''
Validate the following skill document against the provided source material and verify if it is valid.
Focus on:
1. Accuracy: Does the skill capture the technical details correctly based on the Source Material?
2. Structure: Is the skill well-structured according to skill best practices?
3. Completeness: Is any critical information missing in the skill that is present in the Source Material?

Context:
- The skill was originally generated on: $generationDate
- The current evaluation is using model: $modelName
- The instructions used to generate the skill were:
$instructions

Source Material:
$markdown

Current Skill Content:
"$currentSkillContent"
---

Grade the current output based on the instructions and the comparison to current website content and instructions today.
Establish a conclusion on whether the new skill is valid or not.
Reasons for a good or bad quality grade should be provided including concepts such as missing content, different model used, more than a few months old, etc.
On the very last line, output "Grade: [0-100]" representing overall quality of the skill compared to the assumed value if it were generated again today.
''';
}
}
84 changes: 84 additions & 0 deletions tool/lib/src/services/resource_fetcher_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:io' as io;

import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;

import 'markdown_converter.dart';

/// Fetches and converts content from diverse resources.
class ResourceFetcherService {
/// Creates a new [ResourceFetcherService].
ResourceFetcherService({
required http.Client httpClient,
required Logger logger,
}) : _httpClient = httpClient,
_logger = logger;

final http.Client _httpClient;
final Logger _logger;

/// Fetches and converts content from a list of resources.
///
/// Throws an [Exception] if fetching any resource fails. This strict behavior
/// prevents wasting tokens on generating low-quality skills when
/// source material is missing.
Future<String> fetchAndConvertContent(
List<String> resources, {
io.Directory? configDir,
}) async {
final converter = MarkdownConverter();
final sb = StringBuffer();
for (final resource in resources) {
_logger.info(' Fetching $resource...');

if (resource.startsWith('http://')) {
throw Exception(
'Insecure HTTP URL found: $resource. '
'Only HTTPS URLs or relative file paths are allowed.',
);
}

if (resource.startsWith('https://')) {
final response = await _httpClient.get(Uri.parse(resource));
if (response.statusCode == 200) {
sb
..writeln('--- Raw content from $resource ---')
..writeln(converter.convert(response.body));
} else {
throw Exception(
'Failed to fetch $resource: HTTP ${response.statusCode}. '
'Failing fast to save Gemini tokens.',
);
}
} else {
if (configDir == null) {
throw Exception(
'Relative resource "$resource" found, but no configuration '
'directory was provided to resolve it.',
);
}
final file = io.File(p.join(configDir.path, resource));
if (!file.existsSync()) {
throw Exception('Local resource file not found: ${file.path}');
}

final String content;
try {
content = file.readAsStringSync();
} on io.FileSystemException {
throw Exception('Local resource file is not readable: ${file.path}');
}

sb
..writeln('--- Raw content from $resource ---')
..writeln(content);
}
}
return sb.toString();
}
}
Loading
Loading