Visitar URL original
feat: add unified AI prompting and native decision model support · ranadeepsingh/SynapseML@dc563f9 · GitHub
Skip to content

Commit dc563f9

Browse files
feat: add unified AI prompting and native decision model support
Add AIPrompt on the existing OpenAIPrompt interface for explicit chat, Responses, embeddings and native decision-model APIs. Preserve request options, typed results, errors and raw responses through Scala and generated Python, copy, persistence and pipelines. Add the AIPrompt notebook and guide. Keep the contribution focused on AIPrompt and small native specializations. Separate independent legacy defects and optional Delta recovery into their own issues and PRs. Exercise supported notebook examples on packaged artifacts, but leave OpenAIDecisions examples commented because Azure OpenAI/Foundry does not support that API. - Native inference proposal: microsoft#2759 - Deferred recovery: microsoft#2800 - Deferred live Decisions qualification: microsoft#2760 - Global parameter prerequisite: microsoft#2806 - Attachment cancellation prerequisite: microsoft#2807 - Response cleanup prerequisite: microsoft#2808 - Assigned-session prerequisite: microsoft#2809 - Python stub prerequisite: microsoft#2810 - Existing tutorial: docs/Explore Algorithms/OpenAI/OpenAI.ipynb Reuse existing authentication and HTTP infrastructure rather than add a second inference transport. Keep public behavior in Scala and generate Python conveniences without changing legacy public JVM signatures or serialized parameter shapes. Explicit API selection and lossless JSON avoid silent option loss; bounded cancellation handles interrupted Spark work. Keep ordinary inference lazy and replayable, without Delta, an automatic tool loop, response-size configuration or an exactly-once claim. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ba39157-c8dd-4adb-90e0-ff963a1e6733
1 parent 36dfb77 commit dc563f9

75 files changed

Lines changed: 16826 additions & 369 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎cognitive/src/main/scala/com/microsoft/azure/synapse/ml/services/CognitiveServiceBase.scala‎

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,9 @@ object URLEncodingUtils {
329329
}
330330

331331
private[ml] object ServiceAuthHeaders {
332-
private[ml] case class Resolution(headers: Map[String, String], usesFabricFallback: Boolean)
332+
private[ml] case class Resolution(headers: Map[String, String],
333+
usesFabricFallback: Boolean,
334+
usesSubscriptionKey: Boolean)
333335

334336
private[ml] def nonBlank(value: String): Boolean = value != null && value.trim.nonEmpty
335337

@@ -377,20 +379,15 @@ private[ml] object ServiceAuthHeaders {
377379
def isAuthHeaderName(name: String): Boolean =
378380
name.equalsIgnoreCase(subscriptionKeyHeaderName) || name.equalsIgnoreCase(aadHeaderName)
379381

380-
// Resolve exactly one auth header across every credential source in a single, case-insensitive
381-
// precedence step. Blank values are skipped so they cannot suppress a valid lower-priority
382-
// credential, and the automatic Fabric fallback ranks below a credential embedded in
383-
// customHeaders (matching how management-index requests, which have no fallback, resolve auth).
384-
// fabricFallbackAuthHeader is by-name and lowest priority, so it is evaluated only when every
385-
// higher-priority source above is absent. A fallback that acquires a token (and may throw) is
386-
// therefore never run while a subscription key, AAD token, explicit custom-auth header, or an
387-
// embedded customHeaders credential is present.
388-
val explicitAuthHeader: Option[(String, String)] = subscriptionKey.filter(nonBlank)
389-
.map(value => subscriptionKeyHeaderName -> value)
390-
.orElse(aadToken.filter(nonBlank).map(value => aadHeaderName -> ("Bearer " + value)))
391-
.orElse(customAuthHeader.filter(nonBlank).map(value => aadHeaderName -> value))
392-
.orElse(embeddedCredential(providedCustomHeaders, subscriptionKeyHeaderName))
393-
.orElse(embeddedCredential(providedCustomHeaders, aadHeaderName))
382+
// Select one non-blank credential and retain its source even when the auth header names coincide.
383+
// The by-name Fabric fallback is evaluated only when no explicit or embedded credential exists.
384+
val explicitAuth: Option[((String, String), Boolean)] = subscriptionKey.filter(nonBlank)
385+
.map(value => (subscriptionKeyHeaderName -> value) -> true)
386+
.orElse(aadToken.filter(nonBlank).map(value => (aadHeaderName -> ("Bearer " + value)) -> false))
387+
.orElse(customAuthHeader.filter(nonBlank).map(value => (aadHeaderName -> value) -> false))
388+
.orElse(embeddedCredential(providedCustomHeaders, subscriptionKeyHeaderName).map(_ -> true))
389+
.orElse(embeddedCredential(providedCustomHeaders, aadHeaderName).map(_ -> false))
390+
val explicitAuthHeader = explicitAuth.map(_._1)
394391
val fabricAuthHeader = if (explicitAuthHeader.isDefined) {
395392
None
396393
} else {
@@ -421,7 +418,8 @@ private[ml] object ServiceAuthHeaders {
421418

422419
Resolution(
423420
new scala.collection.immutable.TreeMap[String, String]() ++ headers,
424-
usesFabricFallback = fabricAuthHeader.isDefined)
421+
usesFabricFallback = fabricAuthHeader.isDefined,
422+
usesSubscriptionKey = explicitAuth.exists(_._2))
425423
}
426424

427425
def build(subscriptionKey: Option[String],
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (C) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in project root for information.
3+
4+
package com.microsoft.azure.synapse.ml.services.ai
5+
6+
import org.apache.spark.sql.{Column, Row, functions => F}
7+
import org.apache.spark.sql.catalyst.expressions.GenericRowWithSchema
8+
import org.apache.spark.sql.internal.SQLConf
9+
import org.apache.spark.sql.types.{ArrayType, DataType, MapType, StructField, StructType}
10+
11+
import java.util.{HashSet, Set => JavaSet, TreeSet}
12+
13+
private[ai] final case class AIColumnPlan(input: StructType, outputs: Seq[StructField]) {
14+
AIColumnPlan.distinct(input.fieldNames.toSeq, "input columns")
15+
AIColumnPlan.distinct(outputs.map(_.name), "output roles")
16+
require(outputs.forall(_.name.nonEmpty), "output roles must have non-empty names")
17+
private val outputNames = outputs.map(_.name).toSet
18+
private val replacements = input.fields.map(f =>
19+
outputs.find(o => AIColumnPlan.matches(f.name, o.name)))
20+
val schema: StructType = StructType(input.fields.zip(replacements).map {
21+
case (original, replacement) => replacement.getOrElse(original)
22+
} ++ outputs.filterNot(o => input.fields.exists(f => AIColumnPlan.matches(f.name, o.name))))
23+
24+
private val indices = schema.fields.map(f =>
25+
if (outputNames(f.name)) -1 else input.fieldIndex(f.name))
26+
27+
def assemble(row: Row, values: Map[String, Any]): Row =
28+
Row.fromSeq(schema.fields.zip(indices).map { case (field, index) =>
29+
if (index < 0) values.getOrElse(field.name, None.orNull) else row.get(index)
30+
})
31+
32+
def scratch(prefix: String, additional: String*): String =
33+
AIColumnPlan.unused(prefix, schema.fieldNames.toSeq ++ input.fieldNames ++ additional)
34+
}
35+
36+
private[ai] object AIColumnPlan {
37+
def matches(left: String, right: String): Boolean = SQLConf.get.resolver(left, right)
38+
39+
def distinct(names: Seq[String], description: String): Unit = {
40+
require(names.forall(_ != null), s"$description must not have null names")
41+
// This comparator matches Spark's equalsIgnoreCase, including Unicode variants lost by lowercasing.
42+
val seen: JavaSet[String] = if (SQLConf.get.caseSensitiveAnalysis) new HashSet[String]()
43+
else new TreeSet[String](String.CASE_INSENSITIVE_ORDER)
44+
require(!names.exists(name => !seen.add(name)),
45+
s"Ambiguous $description under Spark's column resolver")
46+
}
47+
48+
def resolve(schema: StructType, name: String): String =
49+
schema.fieldNames.find(matches(_, name)).getOrElse(
50+
throw new IllegalArgumentException(s"Input column '$name' does not exist"))
51+
52+
def unused(prefix: String, names: Seq[String]): String =
53+
(Iterator(prefix) ++ Iterator.from(1).map(i => s"${prefix}_$i"))
54+
.find(candidate => !names.exists(matches(_, candidate))).get
55+
56+
def column(name: String): Column = F.col("`" + name.replace("`", "``") + "`")
57+
58+
// Spark can eliminate serialization between Dataset maps and retain external JVM arrays.
59+
def arrayValues(value: Any): scala.collection.Seq[Any] = value match {
60+
case values: scala.collection.Seq[_] => values
61+
case values: Array[_] => values.toSeq
62+
case _ => throw new IllegalArgumentException("Expected a Spark array value")
63+
}
64+
65+
// Fused Dataset maps can pass schema-less Rows directly to the protocol serializers.
66+
def withSchema(row: Row, schema: StructType): Row =
67+
new GenericRowWithSchema(schema.fields.zipWithIndex.map { case (field, index) =>
68+
nestedSchema(row.get(index), field.dataType)
69+
}, schema)
70+
71+
private def nestedSchema(value: Any, dataType: DataType): Any = Option(value).map { present =>
72+
dataType match {
73+
case struct: StructType => withSchema(present.asInstanceOf[Row], struct)
74+
case ArrayType(element, _) =>
75+
arrayValues(present).iterator.map(nestedSchema(_, element)).toVector
76+
case MapType(key, element, _) =>
77+
present.asInstanceOf[scala.collection.Map[Any, Any]].iterator.map { case (k, v) =>
78+
nestedSchema(k, key) -> nestedSchema(v, element)
79+
}.toMap
80+
case _ => present
81+
}
82+
}.orNull
83+
}

0 commit comments

Comments
 (0)