mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 03:53:15 +08:00
Add Host-global thinking and reply language controls.
Inject systemPrompt sections from Netx Ops settings so every preset can switch CoT and final-reply language without changing API/alarm lang. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
53c47fbc6a
commit
d52c05e774
14 changed files with 652 additions and 25 deletions
|
|
@ -155,6 +155,57 @@ function ValueField(props: {
|
|||
)
|
||||
}
|
||||
|
||||
function SelectField(props: {
|
||||
id: string
|
||||
label: string
|
||||
hint: string
|
||||
field: CardFieldState
|
||||
options: ReadonlyArray<{ value: string, label: string }>
|
||||
overriddenLabel: string
|
||||
resetLabel: string
|
||||
invalidLabel: string
|
||||
disabled: boolean
|
||||
onEdit: (text: string) => void
|
||||
onReset: () => void
|
||||
}) {
|
||||
const known = props.options.some((row) => row.value === props.field.text)
|
||||
return (
|
||||
<div className="dsh-nx-settings-field">
|
||||
<div className="dsh-nx-field-head">
|
||||
<label htmlFor={props.id}>{props.label}</label>
|
||||
{props.field.overridden
|
||||
? (
|
||||
<span className="dsh-nx-badges">
|
||||
<span className="dsh-nx-badge">{props.overriddenLabel}</span>
|
||||
<button type="button" className="dsh-nx-reset" disabled={props.disabled} onClick={props.onReset}>
|
||||
{props.resetLabel}
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
<select
|
||||
id={props.id}
|
||||
className={props.field.invalid ? 'dsh-nx-inputInvalid' : undefined}
|
||||
value={props.field.text}
|
||||
disabled={props.disabled}
|
||||
aria-invalid={props.field.invalid || undefined}
|
||||
onChange={(event) => { props.onEdit(event.target.value) }}
|
||||
>
|
||||
{!known && props.field.text
|
||||
? <option value={props.field.text}>{props.field.text}</option>
|
||||
: null}
|
||||
{props.options.map((row) => (
|
||||
<option key={row.value} value={row.value}>{row.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className={props.field.invalid ? 'dsh-nx-invalid' : 'dsh-nx-hint'}>
|
||||
{props.field.invalid ? props.invalidLabel : props.hint}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ImDeliveryPicker(props: {
|
||||
catalog: ImDeliveryCatalog
|
||||
targetsJson: string
|
||||
|
|
@ -435,6 +486,40 @@ export function NetxopsCard(props: NetxopsCardProps) {
|
|||
onEdit={(text) => { props.edit('lang', text) }}
|
||||
onReset={() => { props.resetField('lang') }}
|
||||
/>
|
||||
<SelectField
|
||||
id="netxops-thinking-language"
|
||||
label={t('thinkingLanguage')}
|
||||
hint={t('thinkingLanguageHint')}
|
||||
field={state.thinkingLanguage}
|
||||
options={[
|
||||
{ value: 'auto', label: 'auto' },
|
||||
{ value: 'zh-CN', label: 'zh-CN' },
|
||||
{ value: 'en', label: 'en' },
|
||||
]}
|
||||
overriddenLabel={t('overridden')}
|
||||
resetLabel={t('reset')}
|
||||
invalidLabel={t('invalid')}
|
||||
disabled={disabled}
|
||||
onEdit={(text) => { props.edit('thinkingLanguage', text) }}
|
||||
onReset={() => { props.resetField('thinkingLanguage') }}
|
||||
/>
|
||||
<SelectField
|
||||
id="netxops-reply-language"
|
||||
label={t('replyLanguage')}
|
||||
hint={t('replyLanguageHint')}
|
||||
field={state.replyLanguage}
|
||||
options={[
|
||||
{ value: 'follow-user', label: 'follow-user' },
|
||||
{ value: 'zh', label: 'zh' },
|
||||
{ value: 'en', label: 'en' },
|
||||
]}
|
||||
overriddenLabel={t('overridden')}
|
||||
resetLabel={t('reset')}
|
||||
invalidLabel={t('invalid')}
|
||||
disabled={disabled}
|
||||
onEdit={(text) => { props.edit('replyLanguage', text) }}
|
||||
onReset={() => { props.resetField('replyLanguage') }}
|
||||
/>
|
||||
<ValueField
|
||||
id="netxops-nms-provider"
|
||||
label={t('nmsProvider')}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ const STATUS_POLL_MS = 2_000
|
|||
export interface NetxopsSettings {
|
||||
apiUrl?: string
|
||||
lang?: string
|
||||
thinkingLanguage?: string
|
||||
replyLanguage?: string
|
||||
tokenCredentialRef?: string
|
||||
alarmPushEnabled?: boolean
|
||||
alarmDeliverDsh?: boolean
|
||||
|
|
@ -73,6 +75,8 @@ interface CredentialState {
|
|||
export interface NetxopsCardState extends CardShell {
|
||||
apiUrl: CardFieldState
|
||||
lang: CardFieldState
|
||||
thinkingLanguage: CardFieldState
|
||||
replyLanguage: CardFieldState
|
||||
groupOpsInPreset: CardFieldState
|
||||
groupOpsPublic: CardFieldState
|
||||
groupTopologyInPreset: CardFieldState
|
||||
|
|
@ -172,6 +176,8 @@ export class NetxopsCardController {
|
|||
[
|
||||
textField('apiUrl'),
|
||||
textField('lang'),
|
||||
textField('thinkingLanguage'),
|
||||
textField('replyLanguage'),
|
||||
textField('nmsProvider'),
|
||||
textField('kbRoot'),
|
||||
booleanFieldPersistFalse('groupOpsInPreset'),
|
||||
|
|
@ -521,6 +527,8 @@ export class NetxopsCardController {
|
|||
...this.form.shell(),
|
||||
apiUrl: this.form.field('apiUrl'),
|
||||
lang: this.form.field('lang'),
|
||||
thinkingLanguage: this.form.field('thinkingLanguage'),
|
||||
replyLanguage: this.form.field('replyLanguage'),
|
||||
nmsProvider: this.form.field('nmsProvider'),
|
||||
kbRoot: this.form.field('kbRoot'),
|
||||
groupOpsInPreset: this.form.field('groupOpsInPreset'),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export type NetxopsLocaleKey =
|
|||
| 'apiUrlHint'
|
||||
| 'lang'
|
||||
| 'langHint'
|
||||
| 'thinkingLanguage'
|
||||
| 'thinkingLanguageHint'
|
||||
| 'replyLanguage'
|
||||
| 'replyLanguageHint'
|
||||
| 'capabilityGroups'
|
||||
| 'nmsProvider'
|
||||
| 'nmsProviderHint'
|
||||
|
|
@ -83,8 +87,12 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
|||
sectionExport: 'Session export',
|
||||
apiUrl: 'API URL',
|
||||
apiUrlHint: 'e.g. http://127.0.0.1:8890',
|
||||
lang: 'Language',
|
||||
langHint: 'zh / en',
|
||||
lang: 'API / alarm language',
|
||||
langHint: 'zh / en — netx API query + alarm prompt copy only (not model reply)',
|
||||
thinkingLanguage: 'Thinking language',
|
||||
thinkingLanguageHint: 'auto / zh-CN / en — model chain-of-thought for all presets',
|
||||
replyLanguage: 'Reply language',
|
||||
replyLanguageHint: 'follow-user / zh / en — final answers for all presets',
|
||||
capabilityGroups: 'Capability groups',
|
||||
nmsProvider: 'NMS provider',
|
||||
nmsProviderHint: 'zte-ume',
|
||||
|
|
@ -157,8 +165,12 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
|||
sectionExport: '会话导出',
|
||||
apiUrl: 'API 地址',
|
||||
apiUrlHint: '例如 http://127.0.0.1:8890',
|
||||
lang: '语言',
|
||||
langHint: 'zh / en',
|
||||
lang: 'API / 告警语言',
|
||||
langHint: 'zh / en — 仅影响 netx API 与告警文案,不控制模型回复',
|
||||
thinkingLanguage: '思考语言',
|
||||
thinkingLanguageHint: 'auto / zh-CN / en — 全预设模型思考过程语言',
|
||||
replyLanguage: '回复语言',
|
||||
replyLanguageHint: 'follow-user / zh / en — 全预设最终回答语言',
|
||||
capabilityGroups: '能力组',
|
||||
nmsProvider: 'NMS 提供方',
|
||||
nmsProviderHint: 'zte-ume',
|
||||
|
|
|
|||
|
|
@ -16,11 +16,14 @@ const CSS = `
|
|||
.dsh-nx-field-head>label,.dsh-nx-field-head>.dsh-nx-field-label{flex:1;min-width:0}
|
||||
.dsh-nx-settings-field input[type=text],
|
||||
.dsh-nx-settings-field input[type=password],
|
||||
.dsh-nx-settings-field input:not([type]){
|
||||
.dsh-nx-settings-field input:not([type]),
|
||||
.dsh-nx-settings-field select{
|
||||
box-sizing:border-box;width:100%;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:6px;font:inherit;font-size:13px;line-height:20px;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:inherit
|
||||
}
|
||||
.dsh-nx-settings-field input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:1px}
|
||||
.dsh-nx-settings-field input:disabled{opacity:.55;cursor:default}
|
||||
.dsh-nx-settings-field input:focus-visible,
|
||||
.dsh-nx-settings-field select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:1px}
|
||||
.dsh-nx-settings-field input:disabled,
|
||||
.dsh-nx-settings-field select:disabled{opacity:.55;cursor:default}
|
||||
.dsh-nx-inputInvalid{border-color:var(--dsw-alias-state-error-primary,#d54941)!important}
|
||||
.dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#8f959e)}
|
||||
.dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#d54941)}
|
||||
|
|
|
|||
74
src/index.ts
74
src/index.ts
|
|
@ -1,9 +1,8 @@
|
|||
/**
|
||||
* Host-plane Netx Ops: settings (apiUrl / lang / alarm push / capability groups /
|
||||
* optional knowledge-base root) + credentials (NETX_API_TOKEN) publish a
|
||||
* connection snapshot. The Ops preset mounts selected `netx__*` groups via
|
||||
* `dsh-netxops/tools`; groups marked public also register on the host tool/skill
|
||||
* layer for other presets.
|
||||
* Host-plane Netx Ops: settings (apiUrl / lang / thinking+reply language /
|
||||
* alarm push / capability groups / optional knowledge-base root) + credentials
|
||||
* (NETX_API_TOKEN) publish a connection snapshot. Thinking / reply language
|
||||
* inject into Host `systemPrompt` and apply to **all** agent presets.
|
||||
*
|
||||
* When「关键告警推送」is on, this host dials out to netx's fixed-IP alarm hub and
|
||||
* opens/follows a sticky DSH session (im / WhatsApp is optional and separate).
|
||||
|
|
@ -55,6 +54,15 @@ import {
|
|||
resetKbContext,
|
||||
watchKbContext,
|
||||
} from './netx/kb-runtime.ts'
|
||||
import {
|
||||
normalizeReplyLanguage,
|
||||
normalizeThinkingLanguage,
|
||||
readSystemLocalePreference,
|
||||
replyInstruction,
|
||||
resolveThinkingLanguage,
|
||||
thinkingInstruction,
|
||||
thinkingReminder,
|
||||
} from './netx/model-language.ts'
|
||||
import { publishNetxConnection, getNetxConnection, watchNetxConnection } from './netx/runtime.ts'
|
||||
import {
|
||||
getSessionsExportStatus,
|
||||
|
|
@ -86,8 +94,18 @@ export const DEFAULT_TOKEN_REF = 'NETX_API_TOKEN'
|
|||
export interface Config {
|
||||
/** netx REST root (no trailing slash required). */
|
||||
apiUrl: string
|
||||
/** Passed as `lang` query when starting with `en`. */
|
||||
/** Passed as `lang` query when starting with `en` (API + alarm copy only). */
|
||||
lang: string
|
||||
/**
|
||||
* Model thinking / chain-of-thought language for **all** presets on this Host.
|
||||
* `auto` follows DSH UI locale; otherwise `zh-CN` / `en`.
|
||||
*/
|
||||
thinkingLanguage: string
|
||||
/**
|
||||
* Model final-reply language for **all** presets on this Host.
|
||||
* `follow-user` = no forced reply language; otherwise `zh` / `en`.
|
||||
*/
|
||||
replyLanguage: string
|
||||
/** Credential reference for the bearer token (never store the secret here). */
|
||||
tokenCredentialRef: string
|
||||
/** Per tool-call timeout (ms). */
|
||||
|
|
@ -146,6 +164,8 @@ export interface Config {
|
|||
export const Config: z<Config> = z.object({
|
||||
apiUrl: z.string().default('http://127.0.0.1:8890'),
|
||||
lang: z.string().default('zh'),
|
||||
thinkingLanguage: z.string().default('auto'),
|
||||
replyLanguage: z.string().default('follow-user'),
|
||||
tokenCredentialRef: z.string().role('credential-ref').default(DEFAULT_TOKEN_REF),
|
||||
toolCallTimeoutMs: z.number().step(1).min(1000).default(120_000),
|
||||
installAgentPreset: z.boolean().default(true),
|
||||
|
|
@ -384,6 +404,48 @@ export function apply(ctx: Context, config: Config = Config({})): void {
|
|||
if (String(ref) === source().tokenCredentialRef) publish()
|
||||
})
|
||||
|
||||
// Host-global model language: every preset on this Host inherits these
|
||||
// systemPrompt sections (not scoped to the Netx Ops agent preset).
|
||||
ctx.inject(['systemPrompt', 'settings'], (promptCtx) => {
|
||||
type PromptSurface = {
|
||||
section: (spec: { name: string, order: number, text: string | (() => string) }) => (() => void) | void
|
||||
context: (spec: { name: string, order: number, text: string | (() => string) }) => (() => void) | void
|
||||
}
|
||||
const systemPrompt = (promptCtx as { systemPrompt: PromptSurface }).systemPrompt
|
||||
const effectiveThinking = (): ReturnType<typeof resolveThinkingLanguage> => {
|
||||
const settings = promptCtx.get('settings') as { get?: (ns: string) => unknown } | undefined
|
||||
const current = source()
|
||||
return resolveThinkingLanguage(
|
||||
normalizeThinkingLanguage(current.thinkingLanguage),
|
||||
readSystemLocalePreference(settings),
|
||||
)
|
||||
}
|
||||
promptCtx.effect(() => {
|
||||
const disposers: Array<() => void> = []
|
||||
const push = (dispose: (() => void) | void): void => {
|
||||
if (typeof dispose === 'function') disposers.push(dispose)
|
||||
}
|
||||
push(systemPrompt.section({
|
||||
name: 'netxops:thinking-language',
|
||||
order: 85,
|
||||
text: () => thinkingInstruction(effectiveThinking()),
|
||||
}))
|
||||
push(systemPrompt.context({
|
||||
name: 'netxops:thinking-language-reminder',
|
||||
order: 1000,
|
||||
text: () => thinkingReminder(effectiveThinking()),
|
||||
}))
|
||||
push(systemPrompt.section({
|
||||
name: 'netxops:reply-language',
|
||||
order: 890,
|
||||
text: () => replyInstruction(normalizeReplyLanguage(source().replyLanguage)),
|
||||
}))
|
||||
return () => {
|
||||
for (const dispose of disposers.splice(0)) dispose()
|
||||
}
|
||||
}, 'netxops: model language prompts')
|
||||
})
|
||||
|
||||
// Optional host-layer publish: groups with `public=true` become visible to other presets.
|
||||
ctx.inject(['tools'], (toolsCtx) => {
|
||||
let unregisterTools: (() => void) | undefined
|
||||
|
|
|
|||
144
src/netx/model-language.ts
Normal file
144
src/netx/model-language.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Model thinking / reply language control via systemPrompt injection.
|
||||
* Host-global: applies to every agent preset on this Host.
|
||||
*/
|
||||
|
||||
export const THINKING_LANGUAGE_DEFAULT = 'auto'
|
||||
export const REPLY_LANGUAGE_DEFAULT = 'follow-user'
|
||||
|
||||
/** Thinking-language ids stored in settings (plus `auto`). */
|
||||
export const THINKING_LANGUAGE_IDS = ['zh-CN', 'en'] as const
|
||||
|
||||
/** Reply-language ids stored in settings (plus `follow-user`). */
|
||||
export const REPLY_LANGUAGE_IDS = ['zh', 'en'] as const
|
||||
|
||||
export type ThinkingLanguageId = (typeof THINKING_LANGUAGE_IDS)[number]
|
||||
export type ReplyLanguageId = (typeof REPLY_LANGUAGE_IDS)[number]
|
||||
export type ThinkingLanguageSetting = typeof THINKING_LANGUAGE_DEFAULT | ThinkingLanguageId
|
||||
export type ReplyLanguageSetting = typeof REPLY_LANGUAGE_DEFAULT | ReplyLanguageId
|
||||
|
||||
interface LanguageMeta {
|
||||
id: string
|
||||
name: string
|
||||
native: string
|
||||
}
|
||||
|
||||
const THINKING_META: Record<ThinkingLanguageId, LanguageMeta> = {
|
||||
'zh-CN': { id: 'zh-CN', name: 'Simplified Chinese', native: '简体中文' },
|
||||
en: { id: 'en', name: 'English', native: 'English' },
|
||||
}
|
||||
|
||||
const REPLY_META: Record<ReplyLanguageId, LanguageMeta> = {
|
||||
zh: { id: 'zh', name: 'Simplified Chinese', native: '简体中文' },
|
||||
en: { id: 'en', name: 'English', native: 'English' },
|
||||
}
|
||||
|
||||
/** Normalize a stored thinking-language value. */
|
||||
export function normalizeThinkingLanguage(raw: string | undefined | null): ThinkingLanguageSetting {
|
||||
const value = String(raw ?? '').trim()
|
||||
if (!value || value === THINKING_LANGUAGE_DEFAULT) return THINKING_LANGUAGE_DEFAULT
|
||||
const lower = value.toLowerCase().replace(/_/g, '-')
|
||||
if (lower === 'zh' || lower === 'zh-cn' || lower === 'zh-hans' || lower === 'chinese') return 'zh-CN'
|
||||
if (lower === 'en' || lower.startsWith('en-') || lower === 'english') return 'en'
|
||||
if ((THINKING_LANGUAGE_IDS as readonly string[]).includes(value)) {
|
||||
return value as ThinkingLanguageId
|
||||
}
|
||||
return THINKING_LANGUAGE_DEFAULT
|
||||
}
|
||||
|
||||
/** Normalize a stored reply-language value. */
|
||||
export function normalizeReplyLanguage(raw: string | undefined | null): ReplyLanguageSetting {
|
||||
const value = String(raw ?? '').trim()
|
||||
if (!value || value === REPLY_LANGUAGE_DEFAULT || value === 'auto' || value === 'default') {
|
||||
return REPLY_LANGUAGE_DEFAULT
|
||||
}
|
||||
const lower = value.toLowerCase().replace(/_/g, '-')
|
||||
if (lower === 'zh' || lower === 'zh-cn' || lower === 'zh-hans' || lower === 'chinese') return 'zh'
|
||||
if (lower === 'en' || lower.startsWith('en-') || lower === 'english') return 'en'
|
||||
if ((REPLY_LANGUAGE_IDS as readonly string[]).includes(value)) {
|
||||
return value as ReplyLanguageId
|
||||
}
|
||||
return REPLY_LANGUAGE_DEFAULT
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve effective thinking language.
|
||||
* `auto` follows DSH locale preference, then browser/system tag, then English.
|
||||
*/
|
||||
export function resolveThinkingLanguage(
|
||||
setting: string | undefined | null,
|
||||
systemLocale?: string | null,
|
||||
): ThinkingLanguageId {
|
||||
const normalized = normalizeThinkingLanguage(setting)
|
||||
if (normalized !== THINKING_LANGUAGE_DEFAULT) return normalized
|
||||
return languageFromLocale(systemLocale) ?? 'en'
|
||||
}
|
||||
|
||||
/** Map a BCP-47-ish locale tag to a thinking language. */
|
||||
export function languageFromLocale(locale: string | undefined | null): ThinkingLanguageId | undefined {
|
||||
const tag = String(locale ?? '').trim().toLowerCase().replace(/_/g, '-')
|
||||
if (!tag) return undefined
|
||||
if (tag === 'zh' || tag.startsWith('zh-hans') || tag.startsWith('zh-cn') || tag === 'zh-sg') {
|
||||
return 'zh-CN'
|
||||
}
|
||||
if (tag.startsWith('zh')) return 'zh-CN'
|
||||
if (tag === 'en' || tag.startsWith('en-')) return 'en'
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `locale.preference` from the settings service when present.
|
||||
* Tolerates missing / malformed sections.
|
||||
*/
|
||||
export function readSystemLocalePreference(settings: {
|
||||
get?: (ns: string) => unknown
|
||||
} | undefined | null): string | undefined {
|
||||
if (!settings || typeof settings.get !== 'function') return undefined
|
||||
let section: unknown
|
||||
try {
|
||||
section = settings.get('locale')
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
if (section === null || typeof section !== 'object' || Array.isArray(section)) return undefined
|
||||
const preference = (section as Record<string, unknown>).preference
|
||||
return typeof preference === 'string' && preference.trim() ? preference.trim() : undefined
|
||||
}
|
||||
|
||||
/** System-prompt section text for thinking language (empty when unresolved). */
|
||||
export function thinkingInstruction(language: ThinkingLanguageId): string {
|
||||
const meta = THINKING_META[language]
|
||||
return [
|
||||
`The operator configured the language of your thinking/reasoning process: ${meta.native} (${meta.name}).`,
|
||||
`Write your entire internal reasoning — chain-of-thought, analysis, planning, and deliberation — in ${meta.name}.`,
|
||||
'Keep code, identifiers, file paths, commands, and technical terms as they are.',
|
||||
'Your final answer to the user follows the reply-language setting (or the user language when reply language is follow-user); this thinking setting never alone changes the final answer language.',
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
/** Per-step reminder so switches apply on the next model call in existing sessions. */
|
||||
export function thinkingReminder(language: ThinkingLanguageId): string {
|
||||
const meta = THINKING_META[language]
|
||||
return [
|
||||
`[Thinking language] The operator wants your reasoning/thinking process written in ${meta.native} (${meta.name}).`,
|
||||
`Continue your entire chain-of-thought in ${meta.name}.`,
|
||||
'Final answer language follows the reply-language setting / user language.',
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
/**
|
||||
* System-prompt section for forced reply language.
|
||||
* Returns empty string when `follow-user` (no injection).
|
||||
*/
|
||||
export function replyInstruction(setting: string | undefined | null): string {
|
||||
const normalized = normalizeReplyLanguage(setting)
|
||||
if (normalized === REPLY_LANGUAGE_DEFAULT) return ''
|
||||
const meta = REPLY_META[normalized]
|
||||
return [
|
||||
'## Reply language requirement',
|
||||
`Regardless of the language the user writes in, you must always write the entire reply in ${meta.native} (${meta.name}) —`,
|
||||
'including explanations, summaries, body text, titles, and error descriptions.',
|
||||
'Keep code, commands, file paths, API names, host names, and proper nouns in their original form; do not translate them.',
|
||||
'This requirement outranks language hints in the user message or persona until the Netx Ops reply-language setting changes.',
|
||||
].join(' ')
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue