Skip to content

Commit 87d136a

Browse files
Copilot Reads Summaries from DocGen Tool (#2672)
Adds a Copilot tool called `qsharpGetLibraryDescriptions` which gives Copilot updated descriptions of Q# Library API items, allowing it to make more accurate use of library items in generating Q# snippets and in answering user questions about the libraries. The doc get tool is modified to include a way to generate these summaries in a JSON that is used by the Copilot tool.
1 parent f1a7b8f commit 87d136a

File tree

9 files changed

+311
-36
lines changed

9 files changed

+311
-36
lines changed

source/compiler/qsc_doc_gen/src/generate_docs.rs

Lines changed: 125 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
#[cfg(test)]
55
mod tests;
66

7-
use crate::display::{CodeDisplay, Lookup};
8-
use crate::display::{increase_header_level, parse_doc_for_summary};
7+
use crate::display::{CodeDisplay, Lookup, increase_header_level, parse_doc_for_summary};
98
use crate::table_of_contents::table_of_contents;
109
use qsc_ast::ast;
1110
use qsc_data_structures::language_features::LanguageFeatures;
@@ -15,6 +14,8 @@ use qsc_frontend::resolve;
1514
use qsc_hir::hir::{CallableKind, Item, ItemKind, Package, PackageId, Res, Visibility};
1615
use qsc_hir::{hir, ty};
1716
use rustc_hash::FxHashMap;
17+
use std::collections::BTreeMap;
18+
use std::fmt::Write;
1819
use std::fmt::{Display, Formatter, Result};
1920
use std::rc::Rc;
2021
use std::sync::Arc;
@@ -276,46 +277,46 @@ impl Lookup for Compilation {
276277
}
277278
}
278279

279-
/// Generates and returns documentation files for the standard library
280-
/// and additional sources (if specified.)
281-
#[must_use]
282-
pub fn generate_docs(
283-
additional_sources: Option<(PackageStore, &Dependencies, SourceMap)>,
284-
capabilities: Option<TargetCapabilityFlags>,
285-
language_features: Option<LanguageFeatures>,
286-
) -> Files {
287-
// Capabilities should default to all capabilities for documentation generation.
288-
let capabilities = Some(capabilities.unwrap_or(TargetCapabilityFlags::all()));
289-
let compilation = Compilation::new(additional_sources, capabilities, language_features);
290-
let mut files: FilesWithMetadata = vec![];
280+
/// Determines the package kind for a given package in the compilation context
281+
fn determine_package_kind(package_id: PackageId, compilation: &Compilation) -> Option<PackageKind> {
282+
let is_current_package = compilation.current_package_id == Some(package_id);
283+
284+
if package_id == PackageId::CORE {
285+
// Core package is always included in the compilation.
286+
Some(PackageKind::Core)
287+
} else if package_id == 1.into() {
288+
// Standard package is currently always included, but this isn't enforced by the compiler.
289+
Some(PackageKind::StandardLibrary)
290+
} else if is_current_package {
291+
// This package could be user code if current package is specified.
292+
Some(PackageKind::UserCode)
293+
} else {
294+
// This is a either a direct dependency of the user code or
295+
// is not a package user can access (an indirect dependency).
296+
compilation
297+
.dependencies
298+
.get(&package_id)
299+
.map(|alias| PackageKind::AliasedPackage(alias.to_string()))
300+
}
301+
}
291302

292-
let display = &CodeDisplay {
293-
compilation: &compilation,
294-
};
303+
/// Processes all packages in a compilation and builds a table-of-contents structure
304+
fn build_toc_from_compilation(
305+
compilation: &Compilation,
306+
mut files: Option<&mut FilesWithMetadata>,
307+
) -> ToC {
308+
let display = &CodeDisplay { compilation };
295309

296310
let mut toc: ToC = FxHashMap::default();
297311

298312
for (package_id, unit) in &compilation.package_store {
299-
let is_current_package = compilation.current_package_id == Some(package_id);
300-
let package_kind;
301-
if package_id == PackageId::CORE {
302-
// Core package is always included in the compilation.
303-
package_kind = PackageKind::Core;
304-
} else if package_id == 1.into() {
305-
// Standard package is currently always included, but this isn't enforced by the compiler.
306-
package_kind = PackageKind::StandardLibrary;
307-
} else if is_current_package {
308-
// This package could be user code if current package is specified.
309-
package_kind = PackageKind::UserCode;
310-
} else if let Some(alias) = compilation.dependencies.get(&package_id) {
311-
// This is a direct dependency of the user code.
312-
package_kind = PackageKind::AliasedPackage(alias.to_string());
313-
} else {
314-
// This is not a package user can access (an indirect dependency).
313+
let Some(package_kind) = determine_package_kind(package_id, compilation) else {
315314
continue;
316-
}
315+
};
317316

317+
let is_current_package = compilation.current_package_id == Some(package_id);
318318
let package = &unit.package;
319+
319320
for (_, item) in &package.items {
320321
if let Some((ns, metadata)) = generate_doc_for_item(
321322
package_id,
@@ -324,13 +325,31 @@ pub fn generate_docs(
324325
is_current_package,
325326
item,
326327
display,
327-
&mut files,
328+
files.as_deref_mut().unwrap_or(&mut vec![]),
328329
) {
329330
toc.entry(ns).or_default().push(metadata);
330331
}
331332
}
332333
}
333334

335+
toc
336+
}
337+
338+
/// Generates and returns documentation files for the standard library
339+
/// and additional sources (if specified.)
340+
#[must_use]
341+
pub fn generate_docs(
342+
additional_sources: Option<(PackageStore, &Dependencies, SourceMap)>,
343+
capabilities: Option<TargetCapabilityFlags>,
344+
language_features: Option<LanguageFeatures>,
345+
) -> Files {
346+
// Capabilities should default to all capabilities for documentation generation.
347+
let capabilities = Some(capabilities.unwrap_or(TargetCapabilityFlags::all()));
348+
let compilation = Compilation::new(additional_sources, capabilities, language_features);
349+
let mut files: FilesWithMetadata = vec![];
350+
351+
let mut toc = build_toc_from_compilation(&compilation, Some(&mut files));
352+
334353
// Generate Overview files for each namespace
335354
for (ns, items) in &mut toc {
336355
generate_index_file(&mut files, ns, items);
@@ -709,3 +728,74 @@ fn get_metadata(
709728
signature,
710729
})
711730
}
731+
732+
/// Generates summary documentation organized by namespace.
733+
/// Returns a map of namespace -> metadata items for easier testing and manipulation.
734+
fn generate_summaries_map() -> BTreeMap<String, Vec<Rc<Metadata>>> {
735+
let compilation = Compilation::new(None, None, None);
736+
737+
// Use the shared logic to build ToC structure
738+
let toc = build_toc_from_compilation(&compilation, None);
739+
740+
// Convert ToC to BTreeMap, filtering out table of contents entries
741+
let mut result = BTreeMap::new();
742+
743+
for (ns, items) in toc {
744+
let mut summaries = Vec::new();
745+
746+
for item in items {
747+
// Skip table of contents entries
748+
if item.kind == MetadataKind::TableOfContents {
749+
continue;
750+
}
751+
752+
summaries.push(item);
753+
}
754+
755+
if !summaries.is_empty() {
756+
// Sort items within namespace
757+
summaries.sort_by_key(|item| item.name.clone());
758+
result.insert(ns.to_string(), summaries);
759+
}
760+
}
761+
762+
result
763+
}
764+
/// Converts a Metadata item to its markdown representation
765+
fn metadata_to_markdown(item: &Metadata) -> String {
766+
let mut result = format!("## {}\n\n", item.name);
767+
let _ = write!(result, "```qsharp\n{}\n```\n\n", item.signature);
768+
if !item.summary.is_empty() {
769+
let _ = write!(result, "{}\n\n", item.summary);
770+
}
771+
result
772+
}
773+
774+
/// Generates markdown summary for a single namespace
775+
fn generate_namespace_summary(namespace: &str, items: &[Rc<Metadata>]) -> String {
776+
let mut result = format!("# {namespace}\n\n");
777+
778+
for item in items {
779+
result.push_str(&metadata_to_markdown(item));
780+
}
781+
782+
result
783+
}
784+
785+
/// Generates summary documentation organized by namespace.
786+
/// Returns a single markdown string with namespace headers and minimal item documentation
787+
/// containing just function signatures and summaries for efficient consumption by language models.
788+
#[must_use]
789+
pub fn generate_summaries() -> String {
790+
let summaries_map = generate_summaries_map();
791+
792+
// Generate markdown output organized by namespace
793+
let mut result = String::new();
794+
795+
// Sort namespaces for consistent output (BTreeMap already sorts keys)
796+
for (ns, items) in &summaries_map {
797+
result.push_str(&generate_namespace_summary(ns, items));
798+
}
799+
800+
result
801+
}

source/compiler/qsc_doc_gen/src/generate_docs/tests.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use super::generate_docs;
4+
use crate::generate_docs::{generate_docs, generate_summaries_map, metadata_to_markdown};
55
use expect_test::expect;
6+
use std::collections::BTreeMap;
7+
8+
/// Testing helper function that returns summaries as a structured map
9+
/// for easier test validation. Returns a map where each namespace maps to
10+
/// a vector of markdown strings, one per item.
11+
pub fn generate_summaries_for_testing() -> BTreeMap<String, Vec<String>> {
12+
let summaries_map = generate_summaries_map();
13+
14+
let mut result = BTreeMap::new();
15+
16+
for (ns, items) in summaries_map {
17+
let mut item_markdowns = Vec::new();
18+
19+
for item in items {
20+
let markdown = metadata_to_markdown(&item);
21+
item_markdowns.push(markdown);
22+
}
23+
24+
result.insert(ns, item_markdowns);
25+
}
26+
27+
result
28+
}
629

730
#[test]
831
fn generates_standard_item() {
@@ -207,3 +230,84 @@ fn top_index_file_generation() {
207230
"#]]
208231
.assert_eq(full_contents.as_str());
209232
}
233+
234+
#[test]
235+
fn generates_standard_item_summary() {
236+
let summaries = generate_summaries_for_testing();
237+
// Find a summary for a known item, e.g., Std.Core.Length
238+
let core_summaries = summaries
239+
.get("Std.Core")
240+
.expect("Could not find Std.Core namespace");
241+
let length_summary = core_summaries
242+
.iter()
243+
.find(|item| item.contains("## Length"))
244+
.expect("Could not find summary for Length");
245+
246+
expect![[r#"
247+
## Length
248+
249+
```qsharp
250+
function Length<'T>(a : 'T[]) : Int
251+
```
252+
253+
Returns the number of elements in the input array `a`.
254+
255+
"#]]
256+
.assert_eq(length_summary);
257+
}
258+
259+
#[test]
260+
fn generates_std_core_summary() {
261+
let summaries = generate_summaries_for_testing();
262+
let core_summaries = summaries
263+
.get("Std.Core")
264+
.expect("Could not find Std.Core namespace");
265+
266+
// Combine all summaries for the namespace
267+
let combined_summary = core_summaries.join("\n\n");
268+
269+
expect![[r#"
270+
## Length
271+
272+
```qsharp
273+
function Length<'T>(a : 'T[]) : Int
274+
```
275+
276+
Returns the number of elements in the input array `a`.
277+
278+
279+
280+
## Repeated
281+
282+
```qsharp
283+
function Repeated<'T>(value : 'T, length : Int) : 'T[]
284+
```
285+
286+
Creates an array of given `length` with all elements equal to given `value`. `length` must be a non-negative integer.
287+
288+
"#]]
289+
.assert_eq(&combined_summary);
290+
}
291+
292+
#[test]
293+
fn generates_summary_for_reexport() {
294+
let summaries = generate_summaries_for_testing();
295+
let length_summary = summaries
296+
.get("Microsoft.Quantum.Core")
297+
.expect("Could not find Microsoft.Quantum.Core namespace")
298+
.iter()
299+
.find(|item| item.contains("## Length"))
300+
.expect("Could not find summary for Length");
301+
302+
expect![[r#"
303+
## Length
304+
305+
```qsharp
306+
307+
```
308+
309+
This is an exported item. The actual definition is found here: [Std.Core.Length](xref:Qdk.Std.Core.Length)
310+
311+
"#]]
312+
.assert_eq(length_summary);
313+
}

source/npm/qsharp/src/compiler/compiler.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ export interface ICompiler {
7676

7777
getDocumentation(additionalProgram?: ProgramConfig): Promise<IDocFile[]>;
7878

79+
getLibrarySummaries(): Promise<string>;
80+
7981
checkExerciseSolution(
8082
userCode: string,
8183
exerciseSources: string[],
@@ -249,6 +251,10 @@ export class Compiler implements ICompiler {
249251
);
250252
}
251253

254+
async getLibrarySummaries(): Promise<string> {
255+
return this.wasm.get_library_summaries();
256+
}
257+
252258
async checkExerciseSolution(
253259
userCode: string,
254260
exerciseSources: string[],
@@ -348,6 +354,7 @@ export const compilerProtocol: ServiceProtocol<ICompiler, QscEventData> = {
348354
getEstimates: "request",
349355
getCircuit: "request",
350356
getDocumentation: "request",
357+
getLibrarySummaries: "request",
351358
run: "requestWithProgress",
352359
runWithNoise: "requestWithProgress",
353360
checkExerciseSolution: "requestWithProgress",

source/npm/qsharp/test/basics.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,32 @@ test("autogenerated documentation", async () => {
8484
);
8585
});
8686

87+
test("library summaries slim docs", async () => {
88+
const compiler = getCompiler();
89+
const summaries = await compiler.getLibrarySummaries();
90+
assert(typeof summaries === "string", "Summaries should be a string");
91+
assert(summaries.length > 0, "Summaries should not be empty");
92+
93+
// Check that it contains namespace headers (markdown format)
94+
assert(
95+
summaries.includes("# Microsoft.Quantum"),
96+
"Should contain standard library namespaces",
97+
);
98+
99+
// Check that it contains function signatures in code blocks
100+
assert(summaries.includes("```qsharp"), "Should contain Q# code blocks");
101+
assert(summaries.includes("## "), "Should contain function headers");
102+
103+
// Check that it's organized by namespace
104+
const lines = summaries.split("\n");
105+
const namespaceHeaders = lines.filter((line) => line.startsWith("# "));
106+
assert(namespaceHeaders.length > 0, "Should have namespace headers");
107+
108+
console.log(
109+
`Generated ${summaries.length} characters of summaries with ${namespaceHeaders.length} namespaces`,
110+
);
111+
});
112+
87113
test("basic eval", async () => {
88114
let code = `namespace Test {
89115
function Answer() : Int {

0 commit comments

Comments
 (0)