Skip to content
Draft
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
27 changes: 24 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@
"order": 999
}
},
{
"type": "node",
"request": "launch",
"name": "Compile for azure-linter",
"program": "${workspaceFolder}/packages/compiler/entrypoints/cli.js",
"args": [
"compile",
"C:/t/tspProj/p1/client.tsp",
],
"smartStep": true,
"sourceMaps": true,
"skipFiles": ["<node_internals>/**/*.js"],
"outFiles": [
"${workspaceFolder}/packages/*/dist/**/*.js",
"${workspaceFolder}/packages/*/dist-dev/**/*.js"
],
"cwd": "C:/t/tspProj/p1",
"presentation": {
"order": 2
}
},
{
"type": "node",
"request": "launch",
Expand Down Expand Up @@ -140,12 +161,12 @@

// Set the telemetry key environment variable to use if you dont want to set it in package.json
//"TYPESPEC_VSCODE_TELEMETRY_KEY": "{The instrumentation key of your Application Insights}",

"ENABLE_SERVER_COMPILE_LOGGING": "true",
// "ENABLE_UPDATE_MANAGER_LOGGING": "true",

"TYPESPEC_SERVER_NODE_OPTIONS": "--nolazy --inspect-brk=4242",
"TYPESPEC_DEVELOPMENT_MODE": "true"

// "ENABLE_SERVER_COMPILE_LOGGING": "true",
// "ENABLE_UPDATE_MANAGER_LOGGING": "true"
},
"presentation": {
"hidden": true
Expand Down
44 changes: 44 additions & 0 deletions packages/azure-linter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "azure-linter",
"version": "0.1.0",
"type": "module",
"main": "dist/src/index.js",
"tspMain": "lib/main.tsp",
"exports": {
".": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
},
"./testing": {
"types": "./dist/src/testing/index.d.ts",
"default": "./dist/src/testing/index.js"
}
},
"devDependencies": {
"@types/node": "latest",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@typespec/compiler": "workspace:^",
"@typespec/library-linter": "latest",
"eslint": "^9.15.0",
"prettier": "^3.3.3",
"typescript": "^5.3.3"
},
"scripts": {
"build": "tsc -p ./tsconfig.json",
"watch": "tsc --watch",
"test": "node --test",
"lint": "eslint src/ test/ --report-unused-disable-directives --max-warnings=0",
"lint:fix": "eslint . --report-unused-disable-directives --fix",
"format": "prettier . --write",
"format:check": "prettier --check ."
},
"private": true,
"dependencies": {
"@azure/identity": "~4.10.2",
"jsonrepair": "^3.12.0",
"openai": "^5.9.0",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6"
}
}
2 changes: 2 additions & 0 deletions packages/azure-linter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { $lib } from "./lib.js";
export { $linter } from "./linter.js";
24 changes: 24 additions & 0 deletions packages/azure-linter/src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler";

export const $lib = createTypeSpecLibrary({
name: "azure-linter",
// Define diagnostics for the library. This will provide a typed API to report diagnostic as well as a auto doc generation.
diagnostics: {
"banned-alternate-name": {
severity: "error",
messages: {
default: paramMessage`Banned alternate name "${"name"}".`,
},
},
},
// Defined state keys for storing metadata in decorator.
state: {
alternateName: { description: "alternateName" },
},
});

export const {
reportDiagnostic,
createDiagnostic,
stateKeys: StateKeys,
} = $lib;
27 changes: 27 additions & 0 deletions packages/azure-linter/src/linter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineLinter } from "@typespec/compiler";
import { tooGenericRule } from "./rules/avoid-too-generic-name.naming.csharp.rule.js";
import { booleanPropertyStartsWithVerbRule } from "./rules/boolean-property.naming.csharp.rule.js";
import { durationWithUnitRule } from "./rules/duration-with-unit.naming.csharp.rule.js";
import { noInterfaceRule } from "./rules/no-interfaces.rule.js";

export const $linter = defineLinter({
rules: [noInterfaceRule, booleanPropertyStartsWithVerbRule, durationWithUnitRule, tooGenericRule],
ruleSets: {
recommended: {
enable: {
[`azure-linter/${noInterfaceRule.name}`]: true,
[`azure-linter/${booleanPropertyStartsWithVerbRule.name}`]: true,
[`azure-linter/${durationWithUnitRule.name}`]: true,
[`azure-linter/${tooGenericRule.name}`]: true,
},
},
all: {
enable: {
[`azure-linter/${noInterfaceRule.name}`]: true,
[`azure-linter/${booleanPropertyStartsWithVerbRule.name}`]: true,
[`azure-linter/${durationWithUnitRule.name}`]: true,
[`azure-linter/${tooGenericRule.name}`]: true,
},
},
},
});
104 changes: 104 additions & 0 deletions packages/azure-linter/src/lm/lm-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { ChatMessage } from "@typespec/compiler/experimental";
import z from "zod";
import { logger } from "../log/logger.js";
import { getRecordValue, setRecordValue, tryReadJsonFile, tryWriteFile } from "../utils.js";
import { LmResponseContent } from "./types.js";

const zLmCacheEntry = z.object({
msgKey: z.string().describe("The key for the message, joined by role and message content"),
value: z.any().describe("The cached value, should match the expected response type"),
});

const zLmCache = z
.record(
z.string().describe("The unique identifier of the caller for the cached response"),
z.array(zLmCacheEntry),
)
.describe("Map of cached responses keyed by a unique identifier");

type LmCacheEntry = z.infer<typeof zLmCacheEntry>;
type LmCacheMap = z.infer<typeof zLmCache>;

const MAX_LM_CACHE_SIZE_PER_KEY = 3;

class LmCache {
private _cache: LmCacheMap = {};
private _cacheFilePath?: string;

constructor() {}

async init(cacheFilePath: string) {
if (this._cacheFilePath === undefined) {
this._cacheFilePath = cacheFilePath;
await this.loadFromCacheFile();
} else {
if (this._cacheFilePath !== cacheFilePath) {
logger.warning(
`Cache file path is already set to ${this._cacheFilePath}, ignore the new path ${cacheFilePath}`,
);
}
}

// consider hook on process exit to flush the cache to file
// now we will flush to cache file on every set operation for simplicity
// which should be ignoreable comparing the to actual lm request calls
}

private generateMessageKey(key: string, messages: ChatMessage[]): string {
const msgPart = messages.map((msg) => `${msg.role}:${msg.message}`).join("|");
return `${key}->${msgPart}`;
}

async getForMsg<T extends LmResponseContent>(
callerKey: string,
msg: ChatMessage[],
): Promise<T | undefined> {
const callerCache = getRecordValue(this._cache, callerKey);
if (!callerCache) {
return undefined;
} else {
const msgKey = this.generateMessageKey(callerKey, msg);
const entry = callerCache.find((e) => e.msgKey === msgKey);
if (entry) {
return entry.value as T;
}
return undefined;
}
}

async setForMsg<T extends LmResponseContent>(callerKey: string, msg: ChatMessage[], value: T) {
const msgKey = this.generateMessageKey(callerKey, msg);
const entry: LmCacheEntry = { msgKey, value };

const foundCallerEntry = getRecordValue(this._cache, callerKey);
if (!foundCallerEntry) {
setRecordValue(this._cache, callerKey, [entry]);
} else {
const found = foundCallerEntry.findIndex((e) => e.msgKey === msgKey);
if (found >= 0) {
foundCallerEntry.splice(found, 1);
}
foundCallerEntry.push(entry);
if (foundCallerEntry.length > MAX_LM_CACHE_SIZE_PER_KEY) {
foundCallerEntry.shift();
}
}
await this.flushToFile();
}

private async flushToFile() {
if (!this._cacheFilePath) return;
return tryWriteFile(this._cacheFilePath, JSON.stringify(this._cache, null, 2));
}

private async loadFromCacheFile() {
if (!this._cacheFilePath) {
this._cache = {};
return;
}
const parsed = await tryReadJsonFile(this._cacheFilePath, zLmCache);
this._cache = parsed ?? {};
}
}

export const lmCache = new LmCache();
Loading
Loading