mcard-js
Version:
MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers
176 lines • 7.13 kB
JavaScript
/**
* LLM Runtime Module
*
* Provides LLMRuntime for executing LLM prompts as part of the PTR polyglot runtime system.
* Replicates mcard.ptr.core.llm.runtime.LLMRuntime
*/
import { LLMConfig, DEFAULT_PROVIDER } from './Config.js';
import { OllamaProvider } from './providers/OllamaProvider.js';
import { WebLLMProvider } from './providers/WebLLMProvider.js';
import { MLCLLMProvider } from './providers/MLCLLMProvider.js';
import { IO } from '../../monads/IO.js';
import { Either } from '../../monads/Either.js';
export function get_provider(provider_name = DEFAULT_PROVIDER, base_url = null, timeout = 120) {
if (provider_name === 'ollama') {
return new OllamaProvider(base_url, timeout);
}
if (provider_name === 'webllm') {
return new WebLLMProvider();
}
if (provider_name === 'mlc-llm') {
return new MLCLLMProvider(base_url, timeout);
}
// Future: lmstudio, openai
throw new Error(`Unknown provider: ${provider_name}`);
}
export class LLMRuntime {
provider_name;
_provider = null;
constructor(provider_name = DEFAULT_PROVIDER) {
this.provider_name = provider_name;
}
get provider() {
if (!this._provider) {
this._provider = get_provider(this.provider_name);
}
return this._provider;
}
async execute(codeOrPath, context, config, chapterDir) {
// In Python, execute signature is (concrete_impl, target, context)
// In JS, it is (codeOrPath, context, config, chapterDir)
// If context is a string, it's the prompt, not config overrides.
let configCtx = {};
if (typeof context === 'object' && context !== null) {
configCtx = context;
}
const concrete = config;
// Build configuration
const llmConfig = LLMConfig.from_concrete(concrete, configCtx);
// Update provider if different
if (llmConfig.provider !== this.provider_name) {
this._provider = get_provider(llmConfig.provider, llmConfig.endpoint_url, llmConfig.timeout);
}
// Get prompt from target (context/input)
let prompt = '';
if (typeof context === 'string') {
prompt = context;
}
else {
// If it's an object, we use it as JSON
// Future: support extracting specific field like 'question' or 'prompt' if defined in CLM inputs?
// For now, just dump it.
prompt = JSON.stringify(context);
}
// Execute
let result;
if (llmConfig.system_prompt) {
result = await this._execute_chat(prompt, llmConfig);
}
else {
result = await this._execute_completion(prompt, llmConfig);
}
if (result.isLeft) {
return `Error: ${result.left}`;
}
return this._format_response(result.right, llmConfig);
}
async _execute_completion(prompt, config) {
const params = config.to_provider_params();
return this.provider.complete(prompt, params);
}
async _execute_chat(prompt, config) {
const messages = [];
if (config.system_prompt) {
messages.push({ role: 'system', content: config.system_prompt });
}
messages.push({ role: 'user', content: prompt });
if (config.assistant_instruction) {
messages.push({ role: 'assistant', content: config.assistant_instruction });
}
const params = config.to_provider_params();
return this.provider.chat(messages, params);
}
_format_response(response, config) {
let content = response;
if (response && typeof response === 'object' && 'content' in response) {
content = response.content;
}
if (config.response_format === 'json') {
try {
if (typeof content === 'string') {
const start = content.indexOf('{');
const end = content.lastIndexOf('}') + 1;
if (start >= 0 && end > start) {
return JSON.parse(content.substring(start, end));
}
}
return content;
}
catch (e) {
return content;
}
}
return content;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Monadic Interface Functions
// ─────────────────────────────────────────────────────────────────────────────
/**
* Create a monadic LLM completion execution.
*
* Returns IO<Either<string, any>> for functional composition.
*/
export function promptMonad(prompt, config = {}) {
return IO.of(async () => {
try {
// Instantiate config to get provider and defaults
// We cast config to any because LLMConfig constructor expects Partial<LLMConfig>
// but our partial might miss some index signature stuff if any.
// Actually config is Partial<LLMConfig> so it's fine.
const llmConfig = new LLMConfig(config);
const runtime = new LLMRuntime(llmConfig.provider);
const params = llmConfig.to_provider_params();
return runtime.provider.complete(prompt, params);
}
catch (e) {
return Either.left(`LLM execution failed: ${e}`);
}
});
}
/**
* Create a monadic LLM chat execution.
*
* Returns IO<Either<string, any>> for functional composition.
*/
export function chatMonad(messages = null, prompt = null, system_prompt = "", config = {}) {
return IO.of(async () => {
try {
// Merge system prompt into config if provided
const configData = { ...config };
if (system_prompt)
configData.system_prompt = system_prompt;
const llmConfig = new LLMConfig(configData);
const runtime = new LLMRuntime(llmConfig.provider);
// Build messages if not provided
const msgs = messages ? [...messages] : [];
if (msgs.length === 0) {
if (llmConfig.system_prompt) {
msgs.push({ role: 'system', content: llmConfig.system_prompt });
}
if (prompt) {
msgs.push({ role: 'user', content: prompt });
}
if (llmConfig.assistant_instruction) {
msgs.push({ role: 'assistant', content: llmConfig.assistant_instruction });
}
}
const params = llmConfig.to_provider_params();
return runtime.provider.chat(msgs, params);
}
catch (e) {
return Either.left(`LLM chat failed: ${e}`);
}
});
}
//# sourceMappingURL=LLMRuntime.js.map